@jacebenson/jsn 4.3.9 → 4.3.10-dev.67
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 +11 -539
- package/package.json +1 -1
- package/src/auth.js +81 -38
- package/src/browser-auth.js +162 -0
- package/src/browser-launch.js +148 -0
- package/src/capabilities.js +10 -0
- package/src/cli.js +2 -2
- package/src/commands/auth.js +763 -650
- package/src/commands/flows.js +17 -7
- package/src/commands/midservers.js +1 -1
- package/src/commands/setup.js +25 -20
- package/src/commands/skill.js +1 -1
- package/src/commands/skipped-updates.js +1 -1
- package/src/commands/updatesets.js +14 -2
- package/src/errors.js +8 -0
- package/src/flow-create.js +329 -74
- package/src/flow-designer-interactive.js +11 -6
- package/src/flow-safe-edit.js +48 -0
- package/src/paginated-search.js +2 -2
- package/src/prompts.js +43 -0
package/src/commands/auth.js
CHANGED
|
@@ -1,65 +1,97 @@
|
|
|
1
|
-
import { getEffectiveInstance, normalizeInstanceURL, saveConfig, setActiveProfile
|
|
1
|
+
import { getEffectiveInstance, normalizeInstanceURL, saveConfig, setActiveProfile } from '../config.js';
|
|
2
2
|
import { errUsage, errUsageHint } from '../errors.js';
|
|
3
|
-
import { canUsePicker } from '../helpers.js';
|
|
3
|
+
import { canUsePicker, formatCommandList, quoteShellArg } from '../helpers.js';
|
|
4
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
+
import { refreshSession } from '../session.js';
|
|
7
|
+
|
|
8
|
+
function syncSession(app) {
|
|
9
|
+
if (typeof app._buildSdk === 'function' && typeof app.loadContext === 'function') refreshSession(app);
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
// Auth manages local credentials/profiles — runs without a configured
|
|
8
13
|
// instance. (Not in the daily-check skip-list: legacy behavior checks for
|
|
9
14
|
// updates here.)
|
|
10
|
-
declareCapabilities('auth', { noInstance: true });
|
|
15
|
+
declareCapabilities('auth', { noInstance: true, skipContextHeader: true });
|
|
11
16
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
* default marker, auth ✓/✗, 🔒 read_only, ⚡ skip_confirmations, ✅/⚠️
|
|
15
|
-
* live-verification, and stale-last-seen hints.
|
|
16
|
-
*
|
|
17
|
-
* This view logic lives in the auth command — NOT in src/output.js — and is
|
|
18
|
-
* shipped to the OutputWriter as `data._formatted` (the documented escape
|
|
19
|
-
* hatch for command-curated visuals; see the interface comment in
|
|
20
|
-
* src/output.js). The `summary` line is folded into the formatted string
|
|
21
|
-
* because the writer suppresses opts.summary when _formatted is present.
|
|
22
|
-
* Golden-pinned by test/auth.test.js ("auth status styled output").
|
|
23
|
-
*/
|
|
24
|
-
export function renderAuthStatus(result, summary) {
|
|
25
|
-
let out = summary ? `${summary}\n\n` : '';
|
|
17
|
+
/** Render a compact status table; identity fields require explicit details. */
|
|
18
|
+
export function renderAuthStatus(result, summary, { details = false } = {}) {
|
|
26
19
|
const profiles = Array.isArray(result.profiles) ? result.profiles : [];
|
|
27
|
-
if (profiles.length >
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
20
|
+
if (!profiles.length) return 'No profiles configured.\nRun: jsn auth login <instance> --browser\n';
|
|
21
|
+
const methods = { oauth: 'OAuth', basic: 'Basic', gck: 'Browser session' };
|
|
22
|
+
const states = { available: 'Available', missing: 'Missing', expired: 'Expired', refreshable: 'Refresh required', malformed: 'Invalid' };
|
|
23
|
+
const headings = ['PROFILE', 'AUTH', 'CREDENTIALS', 'CONNECTION', 'ACCESS', 'DELETES', 'COUNTS'];
|
|
24
|
+
if (details) headings.push('USER', 'INSTANCE');
|
|
25
|
+
const notes = [];
|
|
26
|
+
const rows = profiles.map((profile, index) => {
|
|
27
|
+
const diagnostics = profile.diagnostics || {};
|
|
28
|
+
const probe = diagnostics.probe || {};
|
|
29
|
+
const label = profile.name || `Profile ${index + 1}`;
|
|
30
|
+
const connection = profile.verified === true ? 'Verified'
|
|
31
|
+
: probe.code === 'account_mismatch' ? 'User mismatch'
|
|
32
|
+
: profile.verified === false ? 'Failed' : 'Not checked';
|
|
33
|
+
const row = [
|
|
34
|
+
`${label}${profile.active ? ' *' : ''}${profile.default ? ' (default)' : ''}`,
|
|
35
|
+
methods[diagnostics.auth_method] || 'Not configured',
|
|
36
|
+
states[diagnostics.state] || (profile.authenticated ? 'Available' : 'Missing'),
|
|
37
|
+
connection,
|
|
38
|
+
profile.read_only ? 'Read-only' : 'Read/write',
|
|
39
|
+
profile.skip_confirmations ? 'No prompt' : 'Confirm',
|
|
40
|
+
profile.include_counts === false ? 'Off' : 'On',
|
|
41
|
+
];
|
|
42
|
+
if (details) {
|
|
43
|
+
const user = profile.username || profile.verified_as || 'Not recorded';
|
|
44
|
+
row.push(`${user}${profile.verified_as && profile.verified_as !== user ? ` (session: ${profile.verified_as})` : ''}`, profile.instance || 'Not configured');
|
|
45
|
+
}
|
|
46
|
+
if (profile.verified !== true && probe.message) notes.push(`${label}: ${probe.message}`);
|
|
47
|
+
if (profile.name && profile.verified !== true) {
|
|
48
|
+
const command = `jsn auth ${diagnostics.state === 'refreshable' ? 'refresh' : 'login'} --profile ${quoteShellArg(profile.name)}`;
|
|
49
|
+
const hint = ['missing', 'expired', 'refreshable', 'malformed'].includes(diagnostics.state)
|
|
50
|
+
|| ['unauthorized', 'invalid_browser_session', 'refresh_failed', 'account_mismatch'].includes(probe.code)
|
|
51
|
+
? command : probe.hint;
|
|
52
|
+
if (hint) notes.push(`${label}: ${hint}`);
|
|
53
|
+
}
|
|
54
|
+
return row;
|
|
55
|
+
});
|
|
56
|
+
const widths = headings.map((heading, column) => Math.max(heading.length, ...rows.map(row => row[column].length)));
|
|
57
|
+
const formatRow = row => row.map((cell, column) => cell.padEnd(widths[column])).join(' ').trimEnd();
|
|
58
|
+
const lines = [summary || `${profiles.length} profiles`, '', formatRow(headings), ...rows.map(formatRow)];
|
|
59
|
+
if (profiles.some(profile => profile.active)) lines.push('', '* selected profile');
|
|
60
|
+
if (profiles.some(profile => profile.diagnostics?.probe?.code === 'not_requested')) lines.push('', 'Connection not checked (--local).');
|
|
61
|
+
if (notes.length) lines.push('', ...notes);
|
|
62
|
+
if (!details) lines.push('', 'Show users and instance URLs: jsn auth status --details');
|
|
63
|
+
return lines.join('\n') + '\n';
|
|
64
|
+
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
export function profileSettingValues(argv = {}) {
|
|
67
|
+
if (argv['confirm-deletes'] !== undefined && argv['skip-confirmations'] !== undefined
|
|
68
|
+
&& argv['confirm-deletes'] === argv['skip-confirmations']) {
|
|
69
|
+
throw errUsage('--confirm-deletes and --skip-confirmations request conflicting values.');
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...argv,
|
|
73
|
+
'skip-confirmations': argv['confirm-deletes'] !== undefined ? !argv['confirm-deletes'] : argv['skip-confirmations'],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
/** The same profile preferences are offered by setup and new-profile login. */
|
|
78
|
+
export async function configureProfileOptions(profile, argv = {}, { interactive = false, confirm } = {}) {
|
|
79
|
+
argv = profileSettingValues(argv);
|
|
80
|
+
if (interactive && !confirm) ({ confirm } = await import('../prompts.js'));
|
|
81
|
+
const settings = [
|
|
82
|
+
['read_only', 'read-only', 'Read-only profile (blocks mutation commands)?', false],
|
|
83
|
+
['skip_confirmations', 'skip-confirmations', 'Confirm before deleting records?', false],
|
|
84
|
+
['include_counts', 'include-counts', 'Include result totals on list commands?', true],
|
|
85
|
+
];
|
|
86
|
+
for (const [key, flag, message, fallback] of settings) {
|
|
87
|
+
if (argv[flag] !== undefined) profile[key] = argv[flag];
|
|
88
|
+
else if (interactive) {
|
|
89
|
+
const current = profile[key] ?? fallback;
|
|
90
|
+
const value = await confirm({ message, default: key === 'skip_confirmations' ? !current : current });
|
|
91
|
+
profile[key] = key === 'skip_confirmations' ? !value : value;
|
|
60
92
|
}
|
|
61
93
|
}
|
|
62
|
-
return
|
|
94
|
+
return profile;
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
/**
|
|
@@ -88,8 +120,15 @@ export function resolveTargetProfile(argv = {}, cfg = {}, instanceURL = '') {
|
|
|
88
120
|
if (argv.profile && cfg.profiles?.[argv.profile]) {
|
|
89
121
|
return { name: argv.profile, profile: cfg.profiles[argv.profile] };
|
|
90
122
|
}
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
if (argv.instance && cfg.profiles?.[argv.instance]) return { name: argv.instance, profile: cfg.profiles[argv.instance] };
|
|
124
|
+
const matches = Object.entries(cfg.profiles || {})
|
|
125
|
+
.filter(([, profile]) => normalizeInstanceURL(profile.instance_url) === instanceURL);
|
|
126
|
+
if (matches.length > 1) {
|
|
127
|
+
const active = cfg.activeProfile || cfg.defaultProfile;
|
|
128
|
+
if (!argv.instance && matches.some(([name]) => name === active)) return { name: active, profile: cfg.profiles[active] };
|
|
129
|
+
throw errUsage('Multiple profiles use this instance. Choose the user with --profile <name>.');
|
|
130
|
+
}
|
|
131
|
+
const entry = matches[0];
|
|
93
132
|
return entry ? { name: entry[0], profile: entry[1] } : { name: null, profile: null };
|
|
94
133
|
}
|
|
95
134
|
|
|
@@ -100,6 +139,7 @@ export function resolveTargetProfile(argv = {}, cfg = {}, instanceURL = '') {
|
|
|
100
139
|
*/
|
|
101
140
|
export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '', targetProfile = null) {
|
|
102
141
|
if (argv.basic || argv.password) return true;
|
|
142
|
+
if (argv.oauth || argv.code || argv.printUrl || argv.browser || argv.gck || argv.headers) return false;
|
|
103
143
|
if (targetProfile) return targetProfile.auth_method === 'basic';
|
|
104
144
|
return Object.values(cfg.profiles || {}).some(profile =>
|
|
105
145
|
profile.instance_url === instanceURL && profile.auth_method === 'basic'
|
|
@@ -107,7 +147,8 @@ export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '', target
|
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '', targetProfile = null) {
|
|
110
|
-
if (argv.gck || argv.headers) return true;
|
|
150
|
+
if (argv.gck || argv.headers || argv.browser) return true;
|
|
151
|
+
if (argv.basic || argv.password || argv.oauth || argv.code || argv.printUrl) return false;
|
|
111
152
|
if (targetProfile) return targetProfile.auth_method === 'gck';
|
|
112
153
|
return Object.values(cfg.profiles || {}).some(profile =>
|
|
113
154
|
profile.instance_url === instanceURL && profile.auth_method === 'gck'
|
|
@@ -116,7 +157,20 @@ export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '', targetPr
|
|
|
116
157
|
|
|
117
158
|
export function validateLoginSelectors(argv = {}, targetProfile = null) {
|
|
118
159
|
const basic = argv.basic === true || argv.password === true;
|
|
119
|
-
const gck = argv.gck === true || argv.headers !== undefined;
|
|
160
|
+
const gck = argv.gck === true || argv.headers !== undefined || argv.browser === true;
|
|
161
|
+
const oauth = argv.oauth === true || argv.code !== undefined || argv.printUrl === true || argv.waitFile !== undefined;
|
|
162
|
+
if (oauth && (basic || gck)) throw errUsage('Choose only one authentication method: --oauth, --basic, or --browser/--gck.');
|
|
163
|
+
if (argv.waitFile && !argv.printUrl) throw errUsage('--wait-file requires --print-url.');
|
|
164
|
+
if (argv.code !== undefined && argv.printUrl) throw errUsage('--code cannot be combined with --print-url.');
|
|
165
|
+
if (argv.browserUrl !== undefined && !argv.browser) {
|
|
166
|
+
throw errUsage('--browser-url requires --browser.');
|
|
167
|
+
}
|
|
168
|
+
if (argv.browserName !== undefined && (!argv.browser || argv.browserUrl !== undefined)) {
|
|
169
|
+
throw errUsage('--browser-name requires --browser and cannot be combined with --browser-url.');
|
|
170
|
+
}
|
|
171
|
+
if (argv.browser && (argv.headers !== undefined || argv.code !== undefined || argv.printUrl || argv.waitFile)) {
|
|
172
|
+
throw errUsage('--browser cannot be combined with --headers, --code, --print-url, or --wait-file.');
|
|
173
|
+
}
|
|
120
174
|
if (basic && gck) {
|
|
121
175
|
throw errUsage('Basic and browser-session selectors are mutually exclusive; choose --basic/--password or --gck/--headers.');
|
|
122
176
|
}
|
|
@@ -126,6 +180,19 @@ export function validateLoginSelectors(argv = {}, targetProfile = null) {
|
|
|
126
180
|
if (gck && targetProfile?.auth_method === 'basic') {
|
|
127
181
|
throw errUsage('This profile is configured for basic authentication; use --basic/--password or change the profile method.');
|
|
128
182
|
}
|
|
183
|
+
if (oauth && targetProfile?.auth_method && targetProfile.auth_method !== 'oauth') {
|
|
184
|
+
throw errUsage(`This profile is configured for ${targetProfile.auth_method} authentication. Use --name to create a separate OAuth profile.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function loginUsingBrowser(app, instance, argv = {}, username) {
|
|
189
|
+
if (argv.browserUrl) return app.auth.loginWithBrowser(instance, { browserUrl: argv.browserUrl }, username);
|
|
190
|
+
const { chooseBrowser } = await import('../browser-launch.js');
|
|
191
|
+
const browser = await chooseBrowser({ browserName: argv.browserName, interactive: canUsePicker(app, argv) });
|
|
192
|
+
return app.auth.loginWithBrowser(instance, {
|
|
193
|
+
launch: true, browser,
|
|
194
|
+
onProgress: message => process.stderr.write(`${message}\n`),
|
|
195
|
+
}, username);
|
|
129
196
|
}
|
|
130
197
|
|
|
131
198
|
/**
|
|
@@ -134,13 +201,19 @@ export function validateLoginSelectors(argv = {}, targetProfile = null) {
|
|
|
134
201
|
* none configured. A non-interactive caller (agent, pipe, --json/--get)
|
|
135
202
|
* never opens a picker; it falls back to the single/default profile.
|
|
136
203
|
*/
|
|
137
|
-
export async function pickProfile(app, message) {
|
|
138
|
-
const { search } = await import('
|
|
204
|
+
export async function pickProfile(app, message, argv = {}) {
|
|
205
|
+
const { search } = await import('../prompts.js');
|
|
139
206
|
const profileNames = Object.keys(app.config.profiles || {});
|
|
140
207
|
if (profileNames.length === 0) throw errUsage('No profiles configured. Run "jsn setup" first.');
|
|
141
|
-
if (
|
|
208
|
+
if (argv.name || argv.profile) {
|
|
209
|
+
const selected = argv.name || argv.profile;
|
|
210
|
+
if (!app.config.profiles[selected]) throw errUsage(`Profile not found: ${selected}`);
|
|
211
|
+
return selected;
|
|
212
|
+
}
|
|
213
|
+
if (profileNames.length === 1) return profileNames[0];
|
|
214
|
+
if (!canUsePicker(app, argv)) throw errUsageHint(`Which profile? ${profileNames.length} profiles are configured: ${profileNames.join(', ')}`, 'Pass --profile <name>, or jsn auth switch <name> to choose a default.');
|
|
142
215
|
const choices = profileNames.map(name => ({
|
|
143
|
-
name
|
|
216
|
+
name,
|
|
144
217
|
value: name,
|
|
145
218
|
}));
|
|
146
219
|
return search({
|
|
@@ -205,166 +278,48 @@ export async function readBrowserSessionInput() {
|
|
|
205
278
|
});
|
|
206
279
|
}
|
|
207
280
|
|
|
208
|
-
/**
|
|
209
|
-
* Interactive first-run wizard (formerly `jsn setup`).
|
|
210
|
-
*
|
|
211
|
-
* Walks: instance URL → profile name → auth method → optional OAuth login.
|
|
212
|
-
* Creates and activates the profile, then returns the result so the caller
|
|
213
|
-
* decides how to report it. Basic auth saves credentials directly (the OAuth
|
|
214
|
-
* path has no interactive username/password).
|
|
215
|
-
*/
|
|
281
|
+
/** Collect human answers, then call the same login operation as auth login. */
|
|
216
282
|
export async function loginWizard(app, argv = {}) {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const profileName = (await ask('Profile name (default): ')) || 'default';
|
|
245
|
-
const profile = { instance_url: instance };
|
|
246
|
-
if (argv['read-only']) {
|
|
247
|
-
profile.read_only = true;
|
|
248
|
-
}
|
|
249
|
-
if (argv['skip-confirmations']) {
|
|
250
|
-
profile.skip_confirmations = true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const authMethod = await ask('Authentication method (OAuth/Basic/Browser session) [OAuth]: ');
|
|
254
|
-
const useGck = authMethod.toLowerCase().startsWith('g') || authMethod.toLowerCase().startsWith('browser');
|
|
255
|
-
const useBasic = !useGck && authMethod.toLowerCase().startsWith('b');
|
|
256
|
-
|
|
257
|
-
let loggedIn = false;
|
|
258
|
-
if (useBasic) {
|
|
259
|
-
const username = await ask('Username: ');
|
|
260
|
-
rl.close(); // Close outer readline before askHidden creates its own
|
|
261
|
-
const { askHidden } = await import('../auth.js');
|
|
262
|
-
const existingCreds = app.auth.loadCredentials(instance) || app.auth.loadCredentials(instance, username);
|
|
263
|
-
if (existingCreds && existingCreds.auth_method === 'basic') {
|
|
264
|
-
console.log(`Using existing credentials for ${instance}`);
|
|
265
|
-
} else {
|
|
266
|
-
const password = await askHidden('Password: ');
|
|
267
|
-
app.auth.saveCredentials(instance, { auth_method: 'basic', username, password }, username);
|
|
268
|
-
console.log('Basic auth credentials saved');
|
|
269
|
-
}
|
|
270
|
-
profile.username = username;
|
|
271
|
-
profile.auth_method = 'basic';
|
|
272
|
-
} else if (useGck) {
|
|
273
|
-
rl.close();
|
|
274
|
-
const input = await readBrowserSessionInput();
|
|
275
|
-
await app.auth.loginWithGck(instance, input);
|
|
276
|
-
profile.auth_method = 'gck';
|
|
277
|
-
loggedIn = true;
|
|
278
|
-
} else {
|
|
279
|
-
profile.auth_method = 'oauth';
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await setProfile(app.config, profileName, profile);
|
|
283
|
-
// Set as active profile so subsequent commands know which instance to use
|
|
284
|
-
app.config.activeProfile = profileName;
|
|
285
|
-
app.config.defaultProfile = profileName;
|
|
286
|
-
|
|
287
|
-
// Present all connection-level options during setup. Gated on
|
|
288
|
-
// interactivity so a piped/scripted call never blocks on prompts.
|
|
289
|
-
if (canUsePicker(app, argv)) {
|
|
290
|
-
// @inquirer/prompts owns stdin while these confirmations run. Close the
|
|
291
|
-
// older readline interface before handing stdin to Inquirer. This matters
|
|
292
|
-
// on Windows Git Bash: leaving both readers attached can make the later
|
|
293
|
-
// OAuth question see EOF and let Node exit instead of waiting for input.
|
|
294
|
-
rl.close();
|
|
295
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
296
|
-
profile.read_only = await confirm({ message: 'Read-only profile (blocks mutation commands)?', default: profile.read_only || false });
|
|
297
|
-
profile.skip_confirmations = await confirm({ message: 'Skip confirmations (deletes run without prompting)?', default: profile.skip_confirmations || false });
|
|
298
|
-
profile.include_counts = await confirm({ message: 'Include result totals on list commands?', default: profile.include_counts !== false });
|
|
299
|
-
console.log('');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await saveConfig(app.config);
|
|
303
|
-
|
|
304
|
-
if (!useBasic && !useGck) {
|
|
305
|
-
// Recreate readline after Inquirer has released stdin. Do not reuse the
|
|
306
|
-
// interface that was open before the confirmation prompts.
|
|
307
|
-
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
308
|
-
const loginNow = await ask('Login now? [Y/n]: ');
|
|
309
|
-
rl.close();
|
|
310
|
-
if (!loginNow || loginNow.toLowerCase() !== 'n') {
|
|
311
|
-
await app.auth.login(instance);
|
|
312
|
-
// Re-save credentials under <user>@<instance> after verifying username
|
|
313
|
-
try {
|
|
314
|
-
const sdk = app.getSDKForProfile
|
|
315
|
-
? app.getSDKForProfile(instance, { authMethod: 'oauth' })
|
|
316
|
-
: app.sdk;
|
|
317
|
-
const user = await sdk.getCurrentUser();
|
|
318
|
-
if (user && user.user_name) {
|
|
319
|
-
app.auth.migrateLegacyCredential(instance, user.user_name);
|
|
320
|
-
profile.username = user.user_name;
|
|
321
|
-
}
|
|
322
|
-
} catch {
|
|
323
|
-
// Non-fatal — token saved, username will be set on next auth login
|
|
324
|
-
}
|
|
325
|
-
console.log('Login successful!');
|
|
326
|
-
loggedIn = true;
|
|
327
|
-
|
|
328
|
-
// Probe domain separation capability once and stamp the profile, so
|
|
329
|
-
// `jsn domains` (and its help entry) only appear on instances with it.
|
|
330
|
-
try {
|
|
331
|
-
const domainsMod = await import('./domains.js');
|
|
332
|
-
const sdk = app.getSDKForProfile
|
|
333
|
-
? app.getSDKForProfile(instance, {
|
|
334
|
-
profile,
|
|
335
|
-
authMethod: profile.auth_method || 'oauth',
|
|
336
|
-
username: profile.username,
|
|
337
|
-
})
|
|
338
|
-
: app.sdk;
|
|
339
|
-
profile.domain_separation = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
340
|
-
await setProfile(app.config, profileName, profile);
|
|
341
|
-
|
|
342
|
-
// If domain separation is installed, offer to pick a default domain
|
|
343
|
-
// for the profile — every request will be scoped to it via X-Now-Domain.
|
|
344
|
-
if (profile.domain_separation) {
|
|
345
|
-
const { confirm } = await import('@inquirer/prompts');
|
|
346
|
-
const setDomainNow = await confirm({
|
|
347
|
-
message: 'Domain separation detected. Configure a domain for this instance?',
|
|
348
|
-
default: false,
|
|
349
|
-
});
|
|
350
|
-
if (setDomainNow) {
|
|
351
|
-
const domainSysID = await domainsMod.pickDomain({ ...app, sdk });
|
|
352
|
-
if (domainSysID) {
|
|
353
|
-
profile.domain = domainSysID;
|
|
354
|
-
await setProfile(app.config, profileName, profile);
|
|
355
|
-
console.log('Domain configured.');
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
} catch {
|
|
360
|
-
// Non-fatal — capability unknown; domains command falls back to probing
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
rl.close();
|
|
283
|
+
const { input, select, confirm, password } = await import('../prompts.js');
|
|
284
|
+
const explicitInstance = resolveWizardInstance(app, argv);
|
|
285
|
+
const instance = resolveInstanceArg(explicitInstance || await input({
|
|
286
|
+
message: 'ServiceNow instance name or URL:',
|
|
287
|
+
validate: value => value.trim() ? true : 'An instance is required',
|
|
288
|
+
}), app.config);
|
|
289
|
+
const name = argv.name || await input({
|
|
290
|
+
message: 'Profile name:', default: suggestedProfileName(instance),
|
|
291
|
+
validate: value => !value.trim() ? 'A profile name is required'
|
|
292
|
+
: app.config.profiles?.[value.trim()] ? 'That profile already exists; choose another name or sign in again.' : true,
|
|
293
|
+
});
|
|
294
|
+
const method = argv.browser ? 'browser' : argv.basic || argv.password ? 'basic' : await select({
|
|
295
|
+
message: 'Authentication method:',
|
|
296
|
+
choices: [
|
|
297
|
+
{ name: 'Browser — sign in on this computer', value: 'browser' },
|
|
298
|
+
{ name: 'OAuth — authorization code', value: 'oauth' },
|
|
299
|
+
{ name: 'Basic — username and password', value: 'basic' },
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
const request = { ...argv, profile: undefined, instance, name: name.trim() };
|
|
303
|
+
if (method === 'browser') request.browser = true;
|
|
304
|
+
if (method === 'oauth') request.oauth = true;
|
|
305
|
+
if (method === 'basic') {
|
|
306
|
+
request.basic = true;
|
|
307
|
+
const username = await input({ message: 'Username:', validate: value => value.trim() ? true : 'A username is required' });
|
|
308
|
+
request._basicCredentials = { username: username.trim(), password: await password({ message: 'Password:' }) };
|
|
365
309
|
}
|
|
310
|
+
const settings = await configureProfileOptions({}, argv, { interactive: true });
|
|
311
|
+
request['read-only'] = settings.read_only;
|
|
312
|
+
request['skip-confirmations'] = settings.skip_confirmations;
|
|
313
|
+
request['include-counts'] = settings.include_counts;
|
|
314
|
+
request.makeDefault = argv.makeDefault ?? await confirm({
|
|
315
|
+
message: 'Use this profile by default?', default: !app.config.defaultProfile,
|
|
316
|
+
});
|
|
317
|
+
const result = await loginProfile(request, app);
|
|
318
|
+
return { instanceURL: instance, profileName: result.profile, loggedIn: result.authenticated };
|
|
319
|
+
}
|
|
366
320
|
|
|
367
|
-
|
|
321
|
+
function suggestedProfileName(instance) {
|
|
322
|
+
return instance.replace(/https?:\/\//, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
368
323
|
}
|
|
369
324
|
|
|
370
325
|
/**
|
|
@@ -375,7 +330,7 @@ export async function loginWizard(app, argv = {}) {
|
|
|
375
330
|
* Returns { addNew: true, instanceName } or { addNew: false, name }.
|
|
376
331
|
*/
|
|
377
332
|
async function pickLoginTarget(app) {
|
|
378
|
-
const { search, input } = await import('
|
|
333
|
+
const { search, input } = await import('../prompts.js');
|
|
379
334
|
const ADD_NEW = '__add_new__';
|
|
380
335
|
const profileNames = Object.keys(app.config.profiles || {});
|
|
381
336
|
let lastInput = '';
|
|
@@ -386,11 +341,11 @@ async function pickLoginTarget(app) {
|
|
|
386
341
|
lastInput = term || '';
|
|
387
342
|
const profiles = profileNames
|
|
388
343
|
.filter((name) => {
|
|
389
|
-
const label = `${name} — ${app.config.profiles[name].instance_url}`.toLowerCase();
|
|
344
|
+
const label = `${name} — ${app.config.profiles[name].instance_url} ${app.config.profiles[name].username || ''}`.toLowerCase();
|
|
390
345
|
return !filter || label.includes(filter);
|
|
391
346
|
})
|
|
392
347
|
.map((name) => ({
|
|
393
|
-
name: `${name} — ${app.config.profiles[name].instance_url}`,
|
|
348
|
+
name: `${name} — ${app.config.profiles[name].instance_url} (${app.config.profiles[name].username || 'account not recorded'})`,
|
|
394
349
|
value: name,
|
|
395
350
|
}));
|
|
396
351
|
const addLabel = filter
|
|
@@ -414,7 +369,7 @@ async function pickLoginTarget(app) {
|
|
|
414
369
|
/** Per-profile configurable flags (extensible — any boolean on the profile). */
|
|
415
370
|
const PROFILE_FLAGS = [
|
|
416
371
|
{ name: 'Read-only (blocks mutation commands)', value: 'read_only', icon: '🔒' },
|
|
417
|
-
{ name: '
|
|
372
|
+
{ name: 'Confirm before deleting records', value: 'skip_confirmations', icon: '⚡' },
|
|
418
373
|
{ name: 'Include result totals on list commands', value: 'include_counts', icon: '🔢' },
|
|
419
374
|
];
|
|
420
375
|
|
|
@@ -423,9 +378,20 @@ const PROFILE_FLAGS = [
|
|
|
423
378
|
* Interactive picker when no flag passed; testable via argv.flag/argv.domain.
|
|
424
379
|
*/
|
|
425
380
|
export async function modifyProfile(app, argv = {}) {
|
|
426
|
-
|
|
381
|
+
argv = profileSettingValues(argv);
|
|
382
|
+
const name = argv.name || argv.profile || await pickProfile(app, 'Modify which profile?', argv);
|
|
427
383
|
const profile = app.config.profiles?.[name];
|
|
428
384
|
if (!profile) throw new Error(`Profile not found: ${name}`);
|
|
385
|
+
const explicitSettings = ['read-only', 'skip-confirmations', 'include-counts'].some(flag => argv[flag] !== undefined);
|
|
386
|
+
if (explicitSettings) {
|
|
387
|
+
if (argv.domain !== undefined) throw errUsage('Set profile preferences and domain separately.');
|
|
388
|
+
const updated = await configureProfileOptions({ ...profile }, argv);
|
|
389
|
+
app.config.profiles[name] = updated;
|
|
390
|
+
await saveConfig(app.config);
|
|
391
|
+
app.ok({ profile: name, read_only: updated.read_only === true, confirm_deletes: updated.skip_confirmations !== true, include_counts: updated.include_counts !== false },
|
|
392
|
+
{ summary: `Settings saved for ${name}` });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
429
395
|
|
|
430
396
|
// Domain setting path: --domain <name|sys_id|clear>
|
|
431
397
|
if (argv.domain !== undefined) {
|
|
@@ -454,10 +420,12 @@ export async function modifyProfile(app, argv = {}) {
|
|
|
454
420
|
}
|
|
455
421
|
|
|
456
422
|
let flag = argv.flag;
|
|
423
|
+
let picked = false;
|
|
457
424
|
if (!flag) {
|
|
458
|
-
|
|
425
|
+
if (!canUsePicker(app, argv)) throw errUsageHint('Specify the settings to change when noninteractive.', 'Use --read-only, --no-include-counts, or --confirm-deletes.');
|
|
426
|
+
const { select } = await import('../prompts.js');
|
|
459
427
|
const choices = PROFILE_FLAGS.map(f => ({
|
|
460
|
-
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
428
|
+
name: `${f.icon} ${f.name}: ${(f.value === 'include_counts' ? profile[f.value] !== false : f.value === 'skip_confirmations' ? !profile[f.value] : profile[f.value]) ? 'on' : 'off'}`,
|
|
461
429
|
value: f.value,
|
|
462
430
|
}));
|
|
463
431
|
if (profile.domain_separation) {
|
|
@@ -473,13 +441,14 @@ export async function modifyProfile(app, argv = {}) {
|
|
|
473
441
|
message: `Modify ${name} (${profile.instance_url})`,
|
|
474
442
|
choices,
|
|
475
443
|
});
|
|
444
|
+
picked = true;
|
|
476
445
|
if (flag === 'domain') {
|
|
477
446
|
const domainsMod = await import('./domains.js');
|
|
478
447
|
const installed = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
479
448
|
if (!installed) {
|
|
480
449
|
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
481
450
|
}
|
|
482
|
-
const { confirm } = await import('
|
|
451
|
+
const { confirm } = await import('../prompts.js');
|
|
483
452
|
const clear = profile.domain
|
|
484
453
|
? await confirm({ message: `Domain is set. Clear it?`, default: false })
|
|
485
454
|
: false;
|
|
@@ -499,7 +468,17 @@ export async function modifyProfile(app, argv = {}) {
|
|
|
499
468
|
}
|
|
500
469
|
}
|
|
501
470
|
|
|
502
|
-
|
|
471
|
+
if (picked) {
|
|
472
|
+
const setting = { read_only: 'read-only', skip_confirmations: 'confirm-deletes', include_counts: 'include-counts' }[flag];
|
|
473
|
+
const { confirm } = await import('../prompts.js');
|
|
474
|
+
const value = await confirm({
|
|
475
|
+
message: { read_only: 'Read-only profile (blocks mutation commands)?', skip_confirmations: 'Confirm before deleting records?', include_counts: 'Include result totals on list commands?' }[flag],
|
|
476
|
+
default: flag === 'skip_confirmations' ? !profile[flag] : flag === 'include_counts' ? profile[flag] !== false : !!profile[flag],
|
|
477
|
+
});
|
|
478
|
+
return modifyProfile(app, { name, [setting]: value });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
profile[flag] = flag === 'include_counts' ? profile[flag] === false : !profile[flag];
|
|
503
482
|
await saveConfig(app.config);
|
|
504
483
|
app.ok({ profile: name, [flag]: profile[flag] },
|
|
505
484
|
{ summary: `${flag} is now ${profile[flag] ? 'ON' : 'off'} for ${name}` });
|
|
@@ -524,6 +503,7 @@ export async function removeProfile(app, name) {
|
|
|
524
503
|
if (app.config.defaultProfile === name) app.config.defaultProfile = '';
|
|
525
504
|
if (app.config.activeProfile === name) app.config.activeProfile = '';
|
|
526
505
|
saveConfig(app.config);
|
|
506
|
+
syncSession(app);
|
|
527
507
|
|
|
528
508
|
// Delete only this profile's credential identity. Other profiles may share
|
|
529
509
|
// the instance URL but must retain their separate credentials.
|
|
@@ -545,16 +525,522 @@ function canLogoutBareProfile(app, instance, target, explicitlySelected) {
|
|
|
545
525
|
return sameInstance.length <= 1;
|
|
546
526
|
}
|
|
547
527
|
|
|
528
|
+
export async function loginProfile(argv, app) {
|
|
529
|
+
argv = profileSettingValues(argv);
|
|
530
|
+
validateLoginSelectors(argv);
|
|
531
|
+
if (argv.name !== undefined) {
|
|
532
|
+
if (!argv.name.trim()) throw errUsage('--name requires a nonempty profile name.');
|
|
533
|
+
if (argv.profile) throw errUsage('Use --profile to reconnect or --name to create a profile, not both.');
|
|
534
|
+
if (app.config.profiles?.[argv.name]) throw errUsage(`Profile "${argv.name}" already exists. Reconnect with --profile or choose another --name.`);
|
|
535
|
+
}
|
|
536
|
+
let instanceURL;
|
|
537
|
+
let selectedProfileName = argv.profile || (argv.instance && app.config.profiles?.[argv.instance] ? argv.instance : null);
|
|
538
|
+
if (argv.profile) {
|
|
539
|
+
const namedProfile = app.config.profiles?.[argv.profile];
|
|
540
|
+
if (!namedProfile?.instance_url) {
|
|
541
|
+
throw errUsage(`Profile "${argv.profile}" is not configured.`);
|
|
542
|
+
}
|
|
543
|
+
// An explicit profile is authoritative, including in an
|
|
544
|
+
// interactive terminal. Never let the picker retarget it.
|
|
545
|
+
instanceURL = normalizeInstanceURL(namedProfile.instance_url);
|
|
546
|
+
} else if (argv.instance) {
|
|
547
|
+
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
548
|
+
} else if (canUsePicker(app, argv)) {
|
|
549
|
+
// gh-faithful: interactive terminals always get a picker —
|
|
550
|
+
// existing profiles plus "+ Add a new instance". Zero profiles
|
|
551
|
+
// goes straight to the setup wizard.
|
|
552
|
+
const profileNames = Object.keys(app.config.profiles || {});
|
|
553
|
+
if (profileNames.length === 0) {
|
|
554
|
+
return loginWizard(app, argv);
|
|
555
|
+
}
|
|
556
|
+
const target = await pickLoginTarget(app);
|
|
557
|
+
if (target.addNew) {
|
|
558
|
+
return loginWizard(app, { ...argv, instance: resolveInstanceArg(target.instanceName, app.config) });
|
|
559
|
+
} else {
|
|
560
|
+
instanceURL = normalizeInstanceURL(app.config.profiles[target.name].instance_url);
|
|
561
|
+
selectedProfileName = target.name;
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
565
|
+
if (!instanceURL) {
|
|
566
|
+
throw errUsage(`Instance URL required.
|
|
567
|
+
|
|
568
|
+
Examples:
|
|
569
|
+
jsn auth login https://dev12345.service-now.com
|
|
570
|
+
jsn auth login dev373698
|
|
571
|
+
jsn auth login --basic https://dev328604.service-now.com
|
|
572
|
+
|
|
573
|
+
Find your instance URL in your browser's address bar when logged into ServiceNow.`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const matchingProfiles = Object.entries(app.config.profiles || {})
|
|
578
|
+
.filter(([, profile]) => normalizeInstanceURL(profile.instance_url) === instanceURL);
|
|
579
|
+
if (!selectedProfileName && !argv.name && matchingProfiles.length > 1) {
|
|
580
|
+
throw errUsage('Multiple profiles use this instance. Choose the account with: jsn auth login --profile <name>');
|
|
581
|
+
}
|
|
582
|
+
const target = argv.name ? { name: argv.name, profile: null }
|
|
583
|
+
: resolveTargetProfile({ ...argv, profile: selectedProfileName }, app.config, instanceURL);
|
|
584
|
+
const targetProfile = target.profile;
|
|
585
|
+
validateLoginSelectors(argv, targetProfile);
|
|
586
|
+
let targetUsername = targetProfile?.username || '';
|
|
587
|
+
const methodConfig = argv.name ? { profiles: {} } : app.config;
|
|
588
|
+
const useBasic = shouldUseBasicAuth(argv, methodConfig, instanceURL, targetProfile);
|
|
589
|
+
const useGck = shouldUseGckAuth(argv, methodConfig, instanceURL, targetProfile);
|
|
590
|
+
const interactive = canUsePicker(app, argv);
|
|
591
|
+
if (useGck && !argv.browser && !argv.headers && !interactive) {
|
|
592
|
+
throw errUsage('Browser sign-in needs a human. Pass --browser to explicitly open a login window, or supply a captured session.');
|
|
593
|
+
}
|
|
594
|
+
if (!useGck && !useBasic && !argv.printUrl && !argv.code && !interactive) {
|
|
595
|
+
throw errUsage('OAuth login requires --print-url followed by --code <code> when noninteractive.');
|
|
596
|
+
}
|
|
597
|
+
const preliminary = argv.printUrl && !argv.waitFile;
|
|
598
|
+
const preferences = await configureProfileOptions({ ...targetProfile }, argv, {
|
|
599
|
+
interactive: !targetProfile && interactive && !preliminary,
|
|
600
|
+
});
|
|
601
|
+
let setDefault = argv.makeDefault === true;
|
|
602
|
+
if (!targetProfile && argv.makeDefault === undefined && interactive && !preliminary) {
|
|
603
|
+
const { confirm } = await import('../prompts.js');
|
|
604
|
+
setDefault = await confirm({ message: 'Use this profile by default?', default: !app.config.defaultProfile });
|
|
605
|
+
}
|
|
606
|
+
// --basic (with --password as a compatibility alias): authenticate
|
|
607
|
+
// with basic auth from env vars and persist it for this profile.
|
|
608
|
+
if (useGck) {
|
|
609
|
+
const credentials = (argv.browser || (!argv.gck && !argv.headers))
|
|
610
|
+
? await loginUsingBrowser(app, instanceURL, argv, targetUsername || null)
|
|
611
|
+
: await app.auth.loginWithGck(instanceURL, argv.headers || await readBrowserSessionInput(), targetUsername || null);
|
|
612
|
+
targetUsername = credentials?.username || targetUsername;
|
|
613
|
+
} else if (useBasic) {
|
|
614
|
+
let credentials;
|
|
615
|
+
try {
|
|
616
|
+
credentials = argv._basicCredentials
|
|
617
|
+
? await app.auth.loginWithPassword(instanceURL, targetUsername || null, argv._basicCredentials)
|
|
618
|
+
: await app.auth.loginWithPassword(instanceURL, targetUsername || null);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (!interactive || argv._basicCredentials || error.code !== 'auth_error') throw error;
|
|
621
|
+
const { input, password } = await import('../prompts.js');
|
|
622
|
+
const username = targetUsername || await input({ message: 'Username:', validate: value => value.trim() ? true : 'A username is required' });
|
|
623
|
+
credentials = await app.auth.loginWithPassword(instanceURL, targetUsername || null, {
|
|
624
|
+
username, password: await password({ message: 'Password:' }),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// A new profile has no configured username yet. The auth
|
|
628
|
+
// manager saves env/stored credentials under creds.username;
|
|
629
|
+
// carry that identity into verification and profile creation.
|
|
630
|
+
targetUsername = credentials?.username || targetUsername;
|
|
631
|
+
}
|
|
632
|
+
// --print-url with --wait-file: print URL and wait for code file
|
|
633
|
+
else if (argv.printUrl && argv.waitFile) {
|
|
634
|
+
await app.auth.buildAuthURL(instanceURL, argv.waitFile, targetUsername || null);
|
|
635
|
+
}
|
|
636
|
+
// --print-url: just print the URL and exit
|
|
637
|
+
else if (argv.printUrl) {
|
|
638
|
+
const authURL = app.auth.buildAuthURL(instanceURL, undefined, targetUsername || null);
|
|
639
|
+
const result = { authorization_url: authURL, instance: instanceURL, profile: target.name || undefined };
|
|
640
|
+
const targetFlag = argv.name ? `--name ${quoteShellArg(argv.name)}` : target.name ? `--profile ${quoteShellArg(target.name)}` : '';
|
|
641
|
+
const settingsFlags = ['read-only', 'skip-confirmations', 'include-counts']
|
|
642
|
+
.filter(flag => argv[flag] !== undefined).map(flag => `--${argv[flag] ? '' : 'no-'}${flag}`);
|
|
643
|
+
if (argv.makeDefault !== undefined) settingsFlags.push(`--${argv.makeDefault ? '' : 'no-'}make-default`);
|
|
644
|
+
app.ok(result, {
|
|
645
|
+
summary: 'Open the authorization URL, then complete login with the returned code.',
|
|
646
|
+
breadcrumbs: [{ action: 'Complete login', cmd: ['jsn auth login', quoteShellArg(instanceURL), targetFlag, '--code <code>', ...settingsFlags].filter(Boolean).join(' '), description: 'Keep the same profile name and settings when exchanging the code' }],
|
|
647
|
+
});
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
// --code: exchange the provided authorization code directly
|
|
651
|
+
else if (argv.code) {
|
|
652
|
+
await app.auth.loginWithCode(instanceURL, argv.code, targetUsername || null);
|
|
653
|
+
} else {
|
|
654
|
+
// Interactive: full OAuth flow with browser + prompt
|
|
655
|
+
await app.auth.login(instanceURL, targetUsername || null);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Verify auth by fetching current user
|
|
659
|
+
let username = '';
|
|
660
|
+
try {
|
|
661
|
+
const activeName = app.config.activeProfile || app.config.defaultProfile;
|
|
662
|
+
const activeProfile = app.config.profiles?.[activeName];
|
|
663
|
+
const activeInstance = app.getEffectiveInstance?.() || activeProfile?.instance_url || '';
|
|
664
|
+
const activeUsername = activeProfile?.username || '';
|
|
665
|
+
const activeMethod = activeProfile?.auth_method || '';
|
|
666
|
+
const targetMethod = useGck ? 'gck' : useBasic ? 'basic' : 'oauth';
|
|
667
|
+
const targetDiffers = instanceURL !== activeInstance
|
|
668
|
+
|| targetUsername !== activeUsername
|
|
669
|
+
|| targetMethod !== activeMethod;
|
|
670
|
+
if (!app.sdk || targetDiffers) {
|
|
671
|
+
app.sdk = app.getSDKForProfile
|
|
672
|
+
? app.getSDKForProfile(instanceURL, {
|
|
673
|
+
authMethod: targetMethod,
|
|
674
|
+
username: targetUsername,
|
|
675
|
+
profile: targetProfile,
|
|
676
|
+
})
|
|
677
|
+
: app.sdk;
|
|
678
|
+
}
|
|
679
|
+
const user = await app.sdk.getCurrentUser();
|
|
680
|
+
username = user?.user_name || user?.name || '';
|
|
681
|
+
} catch {
|
|
682
|
+
// Non-fatal — token is saved, just couldn't verify
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Basic/GCK auth establishes the target identity before the
|
|
686
|
+
// profile exists; preserve it when verification is unavailable.
|
|
687
|
+
username = username || targetUsername;
|
|
688
|
+
|
|
689
|
+
// Save to profiles — deduplicate by instance URL
|
|
690
|
+
if (!app.config.profiles) {
|
|
691
|
+
app.config.profiles = {};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Preserve an explicitly selected profile even when another profile
|
|
695
|
+
// shares the same instance URL.
|
|
696
|
+
let profileName = target.name;
|
|
697
|
+
if (!profileName) {
|
|
698
|
+
for (const [existingName, existingProfile] of Object.entries(app.config.profiles)) {
|
|
699
|
+
if (existingProfile.instance_url === instanceURL) {
|
|
700
|
+
profileName = existingName;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// If no existing profile found, generate a name from the URL
|
|
707
|
+
if (!profileName) {
|
|
708
|
+
profileName = suggestedProfileName(instanceURL);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
app.config.profiles[profileName] = {
|
|
712
|
+
...(app.config.profiles[profileName] || {}),
|
|
713
|
+
instance_url: instanceURL,
|
|
714
|
+
auth_method: useGck ? 'gck' : useBasic ? 'basic' : 'oauth',
|
|
715
|
+
username: username || undefined,
|
|
716
|
+
read_only: preferences.read_only,
|
|
717
|
+
skip_confirmations: preferences.skip_confirmations,
|
|
718
|
+
include_counts: preferences.include_counts,
|
|
719
|
+
};
|
|
720
|
+
if (!targetProfile && interactive) {
|
|
721
|
+
try {
|
|
722
|
+
const domains = await import('./domains.js');
|
|
723
|
+
const profile = app.config.profiles[profileName];
|
|
724
|
+
const sdk = app.getSDKForProfile
|
|
725
|
+
? app.getSDKForProfile(instanceURL, { profile, authMethod: profile.auth_method, username: profile.username }) : app.sdk;
|
|
726
|
+
profile.domain_separation = await domains.isDomainSeparationInstalled({ sdk });
|
|
727
|
+
if (profile.domain_separation) {
|
|
728
|
+
const { confirm } = await import('../prompts.js');
|
|
729
|
+
if (await confirm({ message: 'Configure a default domain?', default: false })) profile.domain = await domains.pickDomain({ ...app, sdk });
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (error.name === 'PromptCancelled') throw error;
|
|
733
|
+
// Capability discovery remains best-effort, as in setup.
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Re-save OAuth credentials with username now that we have it,
|
|
738
|
+
// so they're keyed by <user>@<instance> for per-user isolation.
|
|
739
|
+
// The legacy bare-instance key is cleaned up behind the
|
|
740
|
+
// AuthManager seam (credential-store internals don't leak here).
|
|
741
|
+
if (!useBasic && username) {
|
|
742
|
+
app.auth.migrateLegacyCredential?.(instanceURL, username);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (setDefault) {
|
|
746
|
+
app.config.instanceURL = instanceURL;
|
|
747
|
+
app.config.defaultProfile = profileName;
|
|
748
|
+
app.config.activeProfile = profileName;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const { saveConfig } = await import('../config.js');
|
|
752
|
+
saveConfig(app.config);
|
|
753
|
+
syncSession(app);
|
|
754
|
+
|
|
755
|
+
const result = {
|
|
756
|
+
authenticated: true,
|
|
757
|
+
instance: instanceURL,
|
|
758
|
+
username: username || undefined,
|
|
759
|
+
profile: profileName,
|
|
760
|
+
default: setDefault || undefined,
|
|
761
|
+
};
|
|
762
|
+
const summary = username
|
|
763
|
+
? `✓ Authenticated to ${instanceURL} as ${username} (profile: ${profileName})`
|
|
764
|
+
: `✓ Authenticated to ${instanceURL} (profile: ${profileName})`;
|
|
765
|
+
const settings = app.config.profiles[profileName];
|
|
766
|
+
const breadcrumbs = !targetProfile ? [
|
|
767
|
+
{
|
|
768
|
+
action: 'Verify this profile', cmd: `jsn --profile ${quoteShellArg(profileName)} auth status`,
|
|
769
|
+
description: 'Check the account and connection for this profile',
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
action: 'Profile settings', cmd: `jsn auth modify ${quoteShellArg(profileName)}`,
|
|
773
|
+
description: `${settings.read_only ? 'read-only' : 'writes allowed'}; ${settings.skip_confirmations ? 'delete confirmations off' : 'confirm deletes'}; counts ${settings.include_counts === false ? 'off' : 'on'}`,
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
action: 'New profile flags', cmd: 'jsn auth login <instance> --read-only --no-include-counts',
|
|
777
|
+
description: 'Set these at login; --skip-confirmations disables delete prompts',
|
|
778
|
+
},
|
|
779
|
+
] : [];
|
|
780
|
+
app.ok(result, { summary, breadcrumbs });
|
|
781
|
+
return result;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function logoutProfile(argv, app) {
|
|
785
|
+
if (!argv.instance && !argv.profile && canUsePicker(app, argv)) {
|
|
786
|
+
argv = { ...argv, profile: await pickProfile(app, 'Sign out of which profile?', argv) };
|
|
787
|
+
}
|
|
788
|
+
let instanceURL;
|
|
789
|
+
if (argv.profile) {
|
|
790
|
+
instanceURL = app.config.profiles?.[argv.profile]?.instance_url;
|
|
791
|
+
if (!instanceURL) throw errUsage(`Profile not found: ${argv.profile}`);
|
|
792
|
+
} else if (argv.instance) {
|
|
793
|
+
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
794
|
+
} else {
|
|
795
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
796
|
+
if (!instanceURL) {
|
|
797
|
+
throw errUsage(`No instance specified.
|
|
798
|
+
|
|
799
|
+
Examples:
|
|
800
|
+
jsn auth logout
|
|
801
|
+
jsn auth logout https://dev12345.service-now.com`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const target = resolveTargetProfile(argv, app.config, instanceURL);
|
|
805
|
+
const username = target.profile?.username ?? null;
|
|
806
|
+
if (username || canLogoutBareProfile(app, instanceURL, target.profile, !!argv.profile)) {
|
|
807
|
+
app.auth.logout(instanceURL, username);
|
|
808
|
+
}
|
|
809
|
+
app.ok({ logged_out: true, instance: instanceURL }, { summary: `✓ Logged out from ${instanceURL}` });
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export async function showAuthStatus(argv, app) {
|
|
813
|
+
const configuredDefault = app.config.profiles?.[app.config.defaultProfile];
|
|
814
|
+
const defaultInstance = configuredDefault?.instance_url || app.config.instanceURL || '';
|
|
815
|
+
|
|
816
|
+
// Check environment auth
|
|
817
|
+
const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
|
|
818
|
+
|
|
819
|
+
const profiles = [];
|
|
820
|
+
const configuredProfiles = Object.entries(app.config.profiles || {});
|
|
821
|
+
const selectedProfiles = argv.profile
|
|
822
|
+
? configuredProfiles.filter(([name]) => name === argv.profile)
|
|
823
|
+
: configuredProfiles;
|
|
824
|
+
for (const [name, profile] of selectedProfiles) {
|
|
825
|
+
const instance = profile.instance_url;
|
|
826
|
+
const authOptions = { authMethod: profile.auth_method, username: profile.username };
|
|
827
|
+
const identityOptions = { username: profile.username, authMethod: profile.auth_method };
|
|
828
|
+
const isAuth = app.auth.isAuthenticatedFor(instance, identityOptions);
|
|
829
|
+
const lastSeen = app.auth.getLastSeen(instance, identityOptions);
|
|
830
|
+
const authState = app.auth.getAuthState(instance, authOptions);
|
|
831
|
+
const legacy = !isAuth && app.auth.hasLegacyCredentials(instance, identityOptions) ? true : undefined;
|
|
832
|
+
// Keep the established status field contract: unauthenticated
|
|
833
|
+
// profiles omit auth_source, except legacy credentials retain
|
|
834
|
+
// their historical `legacy` marker. getAuthState remains the
|
|
835
|
+
// safe metadata seam for authenticated profiles.
|
|
836
|
+
const authSource = isAuth ? authState.auth_source : (legacy ? 'legacy' : undefined);
|
|
837
|
+
|
|
838
|
+
// Try live verification through AuthManager's read-only probe seam.
|
|
839
|
+
let verified = null;
|
|
840
|
+
let verifiedAt = null;
|
|
841
|
+
let probe;
|
|
842
|
+
if (argv.local) {
|
|
843
|
+
probe = { status: 'not_attempted', code: 'not_requested', classification: 'not_checked' };
|
|
844
|
+
} else if (!instance) {
|
|
845
|
+
probe = app.auth.probeUnavailable?.('missing_instance') || { status: 'not_attempted', code: 'missing_instance', classification: 'unavailable' };
|
|
846
|
+
} else {
|
|
847
|
+
try {
|
|
848
|
+
const sdk = app.getSDKForProfile
|
|
849
|
+
? app.getSDKForProfile(instance, { ...authOptions, profile })
|
|
850
|
+
: app.sdk || { getCurrentUser: async () => null };
|
|
851
|
+
let user;
|
|
852
|
+
probe = await app.auth.probeCurrentUser(instance, {
|
|
853
|
+
getCurrentUser: async (options) => {
|
|
854
|
+
user = await sdk.getCurrentUser(options);
|
|
855
|
+
return user;
|
|
856
|
+
},
|
|
857
|
+
}, authOptions);
|
|
858
|
+
if (probe.status === 'succeeded' && user?.user_name) {
|
|
859
|
+
verified = true;
|
|
860
|
+
verifiedAt = user.user_name;
|
|
861
|
+
if (profile.username && profile.username !== user.user_name) {
|
|
862
|
+
verified = false;
|
|
863
|
+
probe = {
|
|
864
|
+
status: 'failed', code: 'account_mismatch', classification: 'account_mismatch',
|
|
865
|
+
message: 'The session account does not match the configured profile account.',
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
} else if (probe.status !== 'not_attempted') {
|
|
869
|
+
verified = false;
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
probe = app.auth.probeUnavailable?.('sdk_construction_failed') || { status: 'not_attempted', code: 'sdk_construction_failed', classification: 'configuration_error' };
|
|
873
|
+
verified = false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Calculate days since last seen
|
|
878
|
+
let daysSinceLastSeen = null;
|
|
879
|
+
if (lastSeen) {
|
|
880
|
+
daysSinceLastSeen = Math.floor((Date.now() / 1000 - lastSeen) / 86400);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
profiles.push({
|
|
884
|
+
name,
|
|
885
|
+
instance,
|
|
886
|
+
username: profile.username || undefined,
|
|
887
|
+
active: name === (app.session?.profileName || app.config.activeProfile || app.config.defaultProfile),
|
|
888
|
+
authenticated: isAuth,
|
|
889
|
+
legacy,
|
|
890
|
+
auth_source: authSource,
|
|
891
|
+
verified,
|
|
892
|
+
verified_as: verifiedAt || undefined,
|
|
893
|
+
last_seen: lastSeen || undefined,
|
|
894
|
+
days_since_last_seen: daysSinceLastSeen,
|
|
895
|
+
stale: daysSinceLastSeen > 7,
|
|
896
|
+
default: name === app.config.defaultProfile,
|
|
897
|
+
read_only: profile.read_only || false,
|
|
898
|
+
skip_confirmations: profile.skip_confirmations || false,
|
|
899
|
+
include_counts: profile.include_counts !== false,
|
|
900
|
+
diagnostics: {
|
|
901
|
+
auth_method: authState.auth_method,
|
|
902
|
+
auth_source: authState.auth_source,
|
|
903
|
+
state: authState.state,
|
|
904
|
+
probe,
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const result = {
|
|
910
|
+
default_instance: defaultInstance,
|
|
911
|
+
authenticated: app.auth.isAuthenticated(),
|
|
912
|
+
environment_auth: envToken ? true : undefined,
|
|
913
|
+
profiles,
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const summary = `${profiles.length} ${profiles.length === 1 ? 'profile' : 'profiles'}`;
|
|
917
|
+
// Ship the styled view as _formatted (see renderAuthStatus); the
|
|
918
|
+
// OutputWriter prints it verbatim and suppresses the summary.
|
|
919
|
+
result._formatted = renderAuthStatus(result, summary, { details: argv.details === true });
|
|
920
|
+
if (!argv.details) {
|
|
921
|
+
delete result.default_instance;
|
|
922
|
+
result.profiles = profiles.map(({ instance: _instance, username: _username, verified_as: _verifiedAs, ...visible }) => visible);
|
|
923
|
+
}
|
|
924
|
+
app.ok(result, {
|
|
925
|
+
summary,
|
|
926
|
+
breadcrumbs: [
|
|
927
|
+
{ action: 'Switch profile', cmd: 'jsn auth switch <name>', description: 'Choose the default profile' },
|
|
928
|
+
{ action: 'Renew session', cmd: 'jsn auth refresh --profile <name>', description: 'Refresh OAuth or sign in again in a browser' },
|
|
929
|
+
{ action: 'Change settings', cmd: 'jsn auth modify --profile <name>', description: 'Read-only access, delete confirmations, and counts' },
|
|
930
|
+
],
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Warn about legacy credentials that need re-authentication
|
|
934
|
+
const legacyProfiles = profiles.filter(p => p.legacy);
|
|
935
|
+
if (legacyProfiles.length > 0) {
|
|
936
|
+
process.stderr.write(
|
|
937
|
+
'\n⚠️ This release changed how credentials are stored.\n' +
|
|
938
|
+
' Some profiles still use the old format.\n' +
|
|
939
|
+
' Run the following to re-authenticate:\n'
|
|
940
|
+
);
|
|
941
|
+
for (const p of legacyProfiles) {
|
|
942
|
+
process.stderr.write(` jsn auth login ${p.name}\n`);
|
|
943
|
+
}
|
|
944
|
+
process.stderr.write('\n');
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
export async function refreshProfile(argv, app) {
|
|
949
|
+
let instanceURL;
|
|
950
|
+
if (argv.profile) {
|
|
951
|
+
instanceURL = app.config.profiles?.[argv.profile]?.instance_url;
|
|
952
|
+
if (!instanceURL) throw errUsage(`Profile not found: ${argv.profile}`);
|
|
953
|
+
} else if (argv.instance) {
|
|
954
|
+
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
955
|
+
} else {
|
|
956
|
+
instanceURL = getEffectiveInstance(app.config);
|
|
957
|
+
if (!instanceURL) {
|
|
958
|
+
throw errUsage(`No instance specified and no default configured.
|
|
959
|
+
|
|
960
|
+
Examples:
|
|
961
|
+
jsn auth refresh
|
|
962
|
+
jsn auth refresh https://dev12345.service-now.com
|
|
963
|
+
jsn auth refresh dev12345`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const target = resolveTargetProfile(argv, app.config, instanceURL);
|
|
968
|
+
const targetProfile = target.profile;
|
|
969
|
+
if (targetProfile?.auth_method === 'gck') {
|
|
970
|
+
return loginProfile({
|
|
971
|
+
...argv, instance: instanceURL, profile: target.name,
|
|
972
|
+
browser: argv.browser || (!argv.headers && canUsePicker(app, argv)),
|
|
973
|
+
}, app);
|
|
974
|
+
}
|
|
975
|
+
const targetAuthOptions = {
|
|
976
|
+
authMethod: targetProfile ? targetProfile.auth_method : 'oauth',
|
|
977
|
+
username: targetProfile?.username,
|
|
978
|
+
};
|
|
979
|
+
const creds = await app.auth.getCredentialsFor(
|
|
980
|
+
instanceURL,
|
|
981
|
+
targetProfile?.username,
|
|
982
|
+
{ ...targetAuthOptions, refresh: false }
|
|
983
|
+
);
|
|
984
|
+
const refreshed = await app.auth.refreshToken(instanceURL, creds, targetProfile?.username ?? null);
|
|
985
|
+
|
|
986
|
+
// Re-probe domain separation on refresh so the capability flag
|
|
987
|
+
// stays in sync if the plugin was installed/removed since setup.
|
|
988
|
+
try {
|
|
989
|
+
const { isDomainSeparationInstalled } = await import('./domains.js');
|
|
990
|
+
const sdk = app.getSDKForProfile
|
|
991
|
+
? app.getSDKForProfile(instanceURL, {
|
|
992
|
+
...targetAuthOptions,
|
|
993
|
+
profile: targetProfile,
|
|
994
|
+
})
|
|
995
|
+
: app.sdk;
|
|
996
|
+
const hasDS = await isDomainSeparationInstalled({ sdk });
|
|
997
|
+
const name = target.name || app.config.activeProfile || app.config.defaultProfile;
|
|
998
|
+
if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
|
|
999
|
+
app.config.profiles[name].domain_separation = hasDS;
|
|
1000
|
+
await saveConfig(app.config);
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
// Non-fatal — capability probe is best-effort
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
app.ok({
|
|
1007
|
+
refreshed: true,
|
|
1008
|
+
instance: instanceURL,
|
|
1009
|
+
expires_at: refreshed.expires_at,
|
|
1010
|
+
}, { summary: `✓ Token refreshed for ${instanceURL}` });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export async function switchProfile(argv, app) {
|
|
1014
|
+
const name = argv.name || argv.profile || await pickProfile(app, 'Use which profile by default?', argv);
|
|
1015
|
+
if (!app.config.profiles?.[name]) throw errUsage(`Profile not found: ${name}`);
|
|
1016
|
+
app.config.instanceURL = app.config.profiles[name].instance_url;
|
|
1017
|
+
await setActiveProfile(app.config, name);
|
|
1018
|
+
syncSession(app);
|
|
1019
|
+
app.ok({ active_profile: name }, { summary: `Default profile: ${name}` });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export async function removeConnection(argv, app) {
|
|
1023
|
+
let name = argv.name;
|
|
1024
|
+
if (!name) {
|
|
1025
|
+
name = await pickProfile(app, 'Remove which profile?', argv);
|
|
1026
|
+
}
|
|
1027
|
+
await removeProfile(app, name);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
548
1030
|
export function authCmd(wrap) {
|
|
549
1031
|
return {
|
|
550
1032
|
command: 'auth [subcommand]',
|
|
551
|
-
describe: 'Manage authentication (OAuth or
|
|
1033
|
+
describe: 'Manage authentication (OAuth, Basic, or browser session)',
|
|
552
1034
|
builder: (yargs) => {
|
|
553
1035
|
return yargs
|
|
554
1036
|
.command({
|
|
555
1037
|
command: 'login [instance]',
|
|
556
1038
|
describe: 'Authenticate with a ServiceNow instance (interactive picker when run bare)',
|
|
557
1039
|
builder: (sub) => sub
|
|
1040
|
+
.option('name', { type: 'string', describe: 'Create a new named profile, including another user on the same instance' })
|
|
1041
|
+
.option('make-default', { type: 'boolean', describe: 'Explicitly use this profile by default' })
|
|
1042
|
+
.option('oauth', { type: 'boolean', describe: 'Authenticate using OAuth authorization code with PKCE' })
|
|
1043
|
+
.option('confirm-deletes', { type: 'boolean', describe: 'Require confirmation before deleting records' })
|
|
558
1044
|
.option('code', {
|
|
559
1045
|
describe: 'Authorization code from browser (bypasses interactive prompt)',
|
|
560
1046
|
type: 'string',
|
|
@@ -572,6 +1058,18 @@ export function authCmd(wrap) {
|
|
|
572
1058
|
describe: 'Browser request headers or cURL command for browser-session auth',
|
|
573
1059
|
type: 'string',
|
|
574
1060
|
})
|
|
1061
|
+
.option('browser', {
|
|
1062
|
+
describe: 'Open a browser and wait for you to log in to ServiceNow',
|
|
1063
|
+
type: 'boolean',
|
|
1064
|
+
})
|
|
1065
|
+
.option('browser-name', {
|
|
1066
|
+
describe: 'Browser to launch: chromium, chrome, or edge (otherwise choose an installed browser)',
|
|
1067
|
+
type: 'string',
|
|
1068
|
+
})
|
|
1069
|
+
.option('browser-url', {
|
|
1070
|
+
describe: 'Advanced: connect to an existing local debugging URL instead of launching (requires --browser)',
|
|
1071
|
+
type: 'string',
|
|
1072
|
+
})
|
|
575
1073
|
.option('print-url', {
|
|
576
1074
|
describe: 'Print the OAuth URL and exit (saves PKCE state for --code)',
|
|
577
1075
|
type: 'boolean',
|
|
@@ -583,446 +1081,60 @@ export function authCmd(wrap) {
|
|
|
583
1081
|
.option('read-only', {
|
|
584
1082
|
describe: 'Mark the created profile as read-only (blocks mutation commands)',
|
|
585
1083
|
type: 'boolean',
|
|
586
|
-
default: false,
|
|
587
1084
|
})
|
|
588
1085
|
.option('skip-confirmations', {
|
|
589
1086
|
describe: 'Skip delete confirmations on this profile (deletes run without prompting)',
|
|
590
1087
|
type: 'boolean',
|
|
591
|
-
default: false,
|
|
592
1088
|
})
|
|
593
1089
|
.option('include-counts', {
|
|
594
1090
|
describe: 'Include result totals on list commands (default on; pass --no-include-counts to opt out)',
|
|
595
1091
|
type: 'boolean',
|
|
596
1092
|
}),
|
|
597
|
-
handler: wrap(
|
|
598
|
-
let instanceURL;
|
|
599
|
-
if (argv.profile) {
|
|
600
|
-
const namedProfile = app.config.profiles?.[argv.profile];
|
|
601
|
-
if (!namedProfile?.instance_url) {
|
|
602
|
-
throw errUsage(`Profile "${argv.profile}" is not configured.`);
|
|
603
|
-
}
|
|
604
|
-
// An explicit profile is authoritative, including in an
|
|
605
|
-
// interactive terminal. Never let the picker retarget it.
|
|
606
|
-
instanceURL = normalizeInstanceURL(namedProfile.instance_url);
|
|
607
|
-
} else if (argv.instance) {
|
|
608
|
-
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
609
|
-
} else if (canUsePicker(app, argv)) {
|
|
610
|
-
// gh-faithful: interactive terminals always get a picker —
|
|
611
|
-
// existing profiles plus "+ Add a new instance". Zero profiles
|
|
612
|
-
// goes straight to the setup wizard.
|
|
613
|
-
const profileNames = Object.keys(app.config.profiles || {});
|
|
614
|
-
if (profileNames.length === 0) {
|
|
615
|
-
const wizard = await loginWizard(app, argv);
|
|
616
|
-
if (wizard.loggedIn) {
|
|
617
|
-
app.ok({ authenticated: true, instance: wizard.instanceURL, profile: wizard.profileName },
|
|
618
|
-
{ summary: `✓ Authenticated to ${wizard.instanceURL} (profile: ${wizard.profileName})` });
|
|
619
|
-
} else {
|
|
620
|
-
app.ok({ setup: true, instance: wizard.instanceURL, profile: wizard.profileName },
|
|
621
|
-
{ summary: `Profile "${wizard.profileName}" created for ${wizard.instanceURL}. Run "jsn auth login" to authenticate.` });
|
|
622
|
-
}
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
const target = await pickLoginTarget(app);
|
|
626
|
-
if (target.addNew) {
|
|
627
|
-
instanceURL = resolveInstanceArg(target.instanceName, app.config);
|
|
628
|
-
} else {
|
|
629
|
-
instanceURL = normalizeInstanceURL(app.config.profiles[target.name].instance_url);
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
instanceURL = getEffectiveInstance(app.config);
|
|
633
|
-
if (!instanceURL) {
|
|
634
|
-
throw errUsage(`Instance URL required.
|
|
635
|
-
|
|
636
|
-
Examples:
|
|
637
|
-
jsn auth login https://dev12345.service-now.com
|
|
638
|
-
jsn auth login dev373698
|
|
639
|
-
jsn auth login --basic https://dev328604.service-now.com
|
|
640
|
-
|
|
641
|
-
Find your instance URL in your browser's address bar when logged into ServiceNow.`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const target = resolveTargetProfile(argv, app.config, instanceURL);
|
|
646
|
-
const targetProfile = target.profile;
|
|
647
|
-
validateLoginSelectors(argv, targetProfile);
|
|
648
|
-
let targetUsername = targetProfile?.username || '';
|
|
649
|
-
const useBasic = shouldUseBasicAuth(argv, app.config, instanceURL, targetProfile);
|
|
650
|
-
const useGck = shouldUseGckAuth(argv, app.config, instanceURL, targetProfile);
|
|
651
|
-
// --basic (with --password as a compatibility alias): authenticate
|
|
652
|
-
// with basic auth from env vars and persist it for this profile.
|
|
653
|
-
if (useGck) {
|
|
654
|
-
const input = argv.headers || await readBrowserSessionInput();
|
|
655
|
-
const credentials = await app.auth.loginWithGck(instanceURL, input, targetUsername || undefined);
|
|
656
|
-
targetUsername = credentials?.username || targetUsername;
|
|
657
|
-
} else if (useBasic) {
|
|
658
|
-
const credentials = await app.auth.loginWithPassword(instanceURL, targetUsername || undefined);
|
|
659
|
-
// A new profile has no configured username yet. The auth
|
|
660
|
-
// manager saves env/stored credentials under creds.username;
|
|
661
|
-
// carry that identity into verification and profile creation.
|
|
662
|
-
targetUsername = credentials?.username || targetUsername;
|
|
663
|
-
}
|
|
664
|
-
// --print-url with --wait-file: print URL and wait for code file
|
|
665
|
-
else if (argv.printUrl && argv.waitFile) {
|
|
666
|
-
await app.auth.buildAuthURL(instanceURL, argv.waitFile, targetUsername || undefined);
|
|
667
|
-
}
|
|
668
|
-
// --print-url: just print the URL and exit
|
|
669
|
-
else if (argv.printUrl) {
|
|
670
|
-
const authURL = app.auth.buildAuthURL(instanceURL);
|
|
671
|
-
console.log(authURL);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
// --code: exchange the provided authorization code directly
|
|
675
|
-
else if (argv.code) {
|
|
676
|
-
await app.auth.loginWithCode(instanceURL, argv.code, targetUsername || undefined);
|
|
677
|
-
} else {
|
|
678
|
-
// Interactive: full OAuth flow with browser + prompt
|
|
679
|
-
await app.auth.login(instanceURL, targetUsername || undefined);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Verify auth by fetching current user
|
|
683
|
-
let username = '';
|
|
684
|
-
try {
|
|
685
|
-
const activeName = app.config.activeProfile || app.config.defaultProfile;
|
|
686
|
-
const activeProfile = app.config.profiles?.[activeName];
|
|
687
|
-
const activeInstance = app.getEffectiveInstance?.() || activeProfile?.instance_url || '';
|
|
688
|
-
const activeUsername = activeProfile?.username || '';
|
|
689
|
-
const activeMethod = activeProfile?.auth_method || '';
|
|
690
|
-
const targetMethod = useGck ? 'gck' : useBasic ? 'basic' : 'oauth';
|
|
691
|
-
const targetDiffers = instanceURL !== activeInstance
|
|
692
|
-
|| targetUsername !== activeUsername
|
|
693
|
-
|| targetMethod !== activeMethod;
|
|
694
|
-
if (!app.sdk || targetDiffers) {
|
|
695
|
-
app.sdk = app.getSDKForProfile
|
|
696
|
-
? app.getSDKForProfile(instanceURL, {
|
|
697
|
-
authMethod: targetMethod,
|
|
698
|
-
username: targetUsername,
|
|
699
|
-
profile: targetProfile,
|
|
700
|
-
})
|
|
701
|
-
: app.sdk;
|
|
702
|
-
}
|
|
703
|
-
const user = await app.sdk.getCurrentUser();
|
|
704
|
-
username = user?.user_name || user?.name || '';
|
|
705
|
-
} catch {
|
|
706
|
-
// Non-fatal — token is saved, just couldn't verify
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Basic/GCK auth establishes the target identity before the
|
|
710
|
-
// profile exists; preserve it when verification is unavailable.
|
|
711
|
-
username = username || targetUsername;
|
|
712
|
-
|
|
713
|
-
// Save to profiles — deduplicate by instance URL
|
|
714
|
-
if (!app.config.profiles) {
|
|
715
|
-
app.config.profiles = {};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Preserve an explicitly selected profile even when another profile
|
|
719
|
-
// shares the same instance URL.
|
|
720
|
-
let profileName = target.name;
|
|
721
|
-
if (!profileName) {
|
|
722
|
-
for (const [existingName, existingProfile] of Object.entries(app.config.profiles)) {
|
|
723
|
-
if (existingProfile.instance_url === instanceURL) {
|
|
724
|
-
profileName = existingName;
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// If no existing profile found, generate a name from the URL
|
|
731
|
-
if (!profileName) {
|
|
732
|
-
profileName = instanceURL.replace(/https?:\/\//, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
app.config.profiles[profileName] = {
|
|
736
|
-
...(app.config.profiles[profileName] || {}),
|
|
737
|
-
instance_url: instanceURL,
|
|
738
|
-
auth_method: useGck ? 'gck' : useBasic ? 'basic' : 'oauth',
|
|
739
|
-
username: username || undefined,
|
|
740
|
-
read_only: argv['read-only'] || undefined,
|
|
741
|
-
skip_confirmations: argv['skip-confirmations'] || undefined,
|
|
742
|
-
include_counts: argv['include-counts'] === true ? true : argv['include-counts'] === false ? false : undefined,
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
// Re-save OAuth credentials with username now that we have it,
|
|
746
|
-
// so they're keyed by <user>@<instance> for per-user isolation.
|
|
747
|
-
// The legacy bare-instance key is cleaned up behind the
|
|
748
|
-
// AuthManager seam (credential-store internals don't leak here).
|
|
749
|
-
if (!useBasic && username) {
|
|
750
|
-
app.auth.migrateLegacyCredential?.(instanceURL, username);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Set as default if this is the first one
|
|
754
|
-
const setDefault = !app.config.instanceURL && !app.config.defaultProfile;
|
|
755
|
-
if (setDefault) {
|
|
756
|
-
app.config.instanceURL = instanceURL;
|
|
757
|
-
app.config.defaultProfile = profileName;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
const { saveConfig } = await import('../config.js');
|
|
761
|
-
saveConfig(app.config);
|
|
762
|
-
|
|
763
|
-
const result = {
|
|
764
|
-
authenticated: true,
|
|
765
|
-
instance: instanceURL,
|
|
766
|
-
username: username || undefined,
|
|
767
|
-
profile: profileName,
|
|
768
|
-
default: setDefault || undefined,
|
|
769
|
-
};
|
|
770
|
-
const summary = username
|
|
771
|
-
? `✓ Authenticated to ${instanceURL} as ${username} (profile: ${profileName})`
|
|
772
|
-
: `✓ Authenticated to ${instanceURL} (profile: ${profileName})`;
|
|
773
|
-
app.ok(result, { summary });
|
|
774
|
-
}),
|
|
1093
|
+
handler: wrap(loginProfile),
|
|
775
1094
|
})
|
|
776
1095
|
.command({
|
|
777
1096
|
command: 'logout [instance]',
|
|
778
1097
|
describe: 'Clear stored credentials (keeps the instance entry — use "auth remove" to delete it)',
|
|
779
|
-
handler: wrap(
|
|
780
|
-
let instanceURL;
|
|
781
|
-
if (argv.instance) {
|
|
782
|
-
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
783
|
-
} else {
|
|
784
|
-
instanceURL = getEffectiveInstance(app.config);
|
|
785
|
-
if (!instanceURL) {
|
|
786
|
-
throw errUsage(`No instance specified.
|
|
787
|
-
|
|
788
|
-
Examples:
|
|
789
|
-
jsn auth logout
|
|
790
|
-
jsn auth logout https://dev12345.service-now.com`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const target = resolveTargetProfile(argv, app.config, instanceURL);
|
|
794
|
-
const username = target.profile?.username ?? null;
|
|
795
|
-
if (username || canLogoutBareProfile(app, instanceURL, target.profile, !!argv.profile)) {
|
|
796
|
-
app.auth.logout(instanceURL, username);
|
|
797
|
-
}
|
|
798
|
-
app.ok({ logged_out: true, instance: instanceURL }, { summary: `✓ Logged out from ${instanceURL}` });
|
|
799
|
-
}),
|
|
1098
|
+
handler: wrap(logoutProfile),
|
|
800
1099
|
})
|
|
801
1100
|
.command({
|
|
802
1101
|
command: 'status',
|
|
803
|
-
describe: 'Show
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
// Check environment auth
|
|
809
|
-
const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
|
|
810
|
-
|
|
811
|
-
const profiles = [];
|
|
812
|
-
const configuredProfiles = Object.entries(app.config.profiles || {});
|
|
813
|
-
const selectedProfiles = argv.profile
|
|
814
|
-
? configuredProfiles.filter(([name]) => name === argv.profile)
|
|
815
|
-
: configuredProfiles;
|
|
816
|
-
for (const [name, profile] of selectedProfiles) {
|
|
817
|
-
const instance = profile.instance_url;
|
|
818
|
-
const authOptions = { authMethod: profile.auth_method, username: profile.username };
|
|
819
|
-
const identityOptions = { username: profile.username, authMethod: profile.auth_method };
|
|
820
|
-
const isAuth = app.auth.isAuthenticatedFor(instance, identityOptions);
|
|
821
|
-
const lastSeen = app.auth.getLastSeen(instance, identityOptions);
|
|
822
|
-
const authState = app.auth.getAuthState(instance, authOptions);
|
|
823
|
-
const legacy = !isAuth && app.auth.hasLegacyCredentials(instance, identityOptions) ? true : undefined;
|
|
824
|
-
// Keep the established status field contract: unauthenticated
|
|
825
|
-
// profiles omit auth_source, except legacy credentials retain
|
|
826
|
-
// their historical `legacy` marker. getAuthState remains the
|
|
827
|
-
// safe metadata seam for authenticated profiles.
|
|
828
|
-
const authSource = isAuth ? authState.auth_source : (legacy ? 'legacy' : undefined);
|
|
829
|
-
|
|
830
|
-
// Try live verification through AuthManager's read-only probe seam.
|
|
831
|
-
let verified = null;
|
|
832
|
-
let verifiedAt = null;
|
|
833
|
-
let probe;
|
|
834
|
-
if (!instance) {
|
|
835
|
-
probe = app.auth.probeUnavailable?.('missing_instance') || { status: 'not_attempted', code: 'missing_instance', classification: 'unavailable' };
|
|
836
|
-
} else {
|
|
837
|
-
try {
|
|
838
|
-
const sdk = app.getSDKForProfile
|
|
839
|
-
? app.getSDKForProfile(instance, { ...authOptions, profile })
|
|
840
|
-
: app.sdk || { getCurrentUser: async () => null };
|
|
841
|
-
let user;
|
|
842
|
-
probe = await app.auth.probeCurrentUser(instance, {
|
|
843
|
-
getCurrentUser: async (options) => {
|
|
844
|
-
user = await sdk.getCurrentUser(options);
|
|
845
|
-
return user;
|
|
846
|
-
},
|
|
847
|
-
}, authOptions);
|
|
848
|
-
if (probe.status === 'succeeded' && user?.user_name) {
|
|
849
|
-
verified = true;
|
|
850
|
-
verifiedAt = user.user_name;
|
|
851
|
-
} else if (probe.status !== 'not_attempted') {
|
|
852
|
-
verified = false;
|
|
853
|
-
}
|
|
854
|
-
} catch {
|
|
855
|
-
probe = app.auth.probeUnavailable?.('sdk_construction_failed') || { status: 'not_attempted', code: 'sdk_construction_failed', classification: 'configuration_error' };
|
|
856
|
-
verified = false;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Calculate days since last seen
|
|
861
|
-
let daysSinceLastSeen = null;
|
|
862
|
-
if (lastSeen) {
|
|
863
|
-
daysSinceLastSeen = Math.floor((Date.now() / 1000 - lastSeen) / 86400);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
profiles.push({
|
|
867
|
-
name,
|
|
868
|
-
instance,
|
|
869
|
-
authenticated: isAuth,
|
|
870
|
-
legacy,
|
|
871
|
-
auth_source: authSource,
|
|
872
|
-
verified,
|
|
873
|
-
verified_as: verifiedAt || undefined,
|
|
874
|
-
last_seen: lastSeen || undefined,
|
|
875
|
-
days_since_last_seen: daysSinceLastSeen,
|
|
876
|
-
stale: daysSinceLastSeen > 7,
|
|
877
|
-
default: name === app.config.defaultProfile,
|
|
878
|
-
read_only: profile.read_only || false,
|
|
879
|
-
skip_confirmations: profile.skip_confirmations || false,
|
|
880
|
-
include_counts: profile.include_counts !== false,
|
|
881
|
-
diagnostics: {
|
|
882
|
-
auth_method: authState.auth_method,
|
|
883
|
-
auth_source: authState.auth_source,
|
|
884
|
-
state: authState.state,
|
|
885
|
-
probe,
|
|
886
|
-
},
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const result = {
|
|
891
|
-
default_instance: defaultInstance,
|
|
892
|
-
authenticated: app.auth.isAuthenticated(),
|
|
893
|
-
environment_auth: envToken ? true : undefined,
|
|
894
|
-
profiles,
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
const summary = `${profiles.length} profile(s)`;
|
|
898
|
-
// Ship the styled view as _formatted (see renderAuthStatus); the
|
|
899
|
-
// OutputWriter prints it verbatim and suppresses the summary.
|
|
900
|
-
result._formatted = renderAuthStatus(result, summary);
|
|
901
|
-
app.ok(result, { summary });
|
|
902
|
-
|
|
903
|
-
// Warn about legacy credentials that need re-authentication
|
|
904
|
-
const legacyProfiles = profiles.filter(p => p.legacy);
|
|
905
|
-
if (legacyProfiles.length > 0) {
|
|
906
|
-
process.stderr.write(
|
|
907
|
-
'\n⚠️ This release changed how credentials are stored.\n' +
|
|
908
|
-
' Some profiles still use the old format.\n' +
|
|
909
|
-
' Run the following to re-authenticate:\n'
|
|
910
|
-
);
|
|
911
|
-
for (const p of legacyProfiles) {
|
|
912
|
-
process.stderr.write(` jsn auth login ${p.name}\n`);
|
|
913
|
-
}
|
|
914
|
-
process.stderr.write('\n');
|
|
915
|
-
}
|
|
1102
|
+
describe: 'Show authentication status (use --details for users and instance URLs)',
|
|
1103
|
+
builder: sub => sub.option('local', {
|
|
1104
|
+
type: 'boolean', describe: 'Show saved credential state without contacting any instance',
|
|
1105
|
+
}).option('details', {
|
|
1106
|
+
type: 'boolean', describe: 'Include users and instance URLs in status output',
|
|
916
1107
|
}),
|
|
1108
|
+
handler: wrap(showAuthStatus),
|
|
917
1109
|
})
|
|
918
1110
|
.command({
|
|
919
1111
|
command: 'refresh [instance]',
|
|
920
|
-
describe: 'Refresh OAuth
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (!instanceURL) {
|
|
928
|
-
throw errUsage(`No instance specified and no default configured.
|
|
929
|
-
|
|
930
|
-
Examples:
|
|
931
|
-
jsn auth refresh
|
|
932
|
-
jsn auth refresh https://dev12345.service-now.com
|
|
933
|
-
jsn auth refresh dev12345`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const target = resolveTargetProfile(argv, app.config, instanceURL);
|
|
938
|
-
const targetProfile = target.profile;
|
|
939
|
-
const targetAuthOptions = {
|
|
940
|
-
authMethod: targetProfile ? targetProfile.auth_method : 'oauth',
|
|
941
|
-
username: targetProfile?.username,
|
|
942
|
-
};
|
|
943
|
-
const creds = await app.auth.getCredentialsFor(
|
|
944
|
-
instanceURL,
|
|
945
|
-
targetProfile?.username,
|
|
946
|
-
targetAuthOptions
|
|
947
|
-
);
|
|
948
|
-
const refreshed = await app.auth.refreshToken(instanceURL, creds, targetProfile?.username ?? null);
|
|
949
|
-
|
|
950
|
-
// Re-probe domain separation on refresh so the capability flag
|
|
951
|
-
// stays in sync if the plugin was installed/removed since setup.
|
|
952
|
-
try {
|
|
953
|
-
const { isDomainSeparationInstalled } = await import('./domains.js');
|
|
954
|
-
const sdk = app.getSDKForProfile
|
|
955
|
-
? app.getSDKForProfile(instanceURL, {
|
|
956
|
-
...targetAuthOptions,
|
|
957
|
-
profile: targetProfile,
|
|
958
|
-
})
|
|
959
|
-
: app.sdk;
|
|
960
|
-
const hasDS = await isDomainSeparationInstalled({ sdk });
|
|
961
|
-
const name = target.name || app.config.activeProfile || app.config.defaultProfile;
|
|
962
|
-
if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
|
|
963
|
-
app.config.profiles[name].domain_separation = hasDS;
|
|
964
|
-
await saveConfig(app.config);
|
|
965
|
-
}
|
|
966
|
-
} catch {
|
|
967
|
-
// Non-fatal — capability probe is best-effort
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
app.ok({
|
|
971
|
-
refreshed: true,
|
|
972
|
-
instance: instanceURL,
|
|
973
|
-
expires_at: refreshed.expires_at,
|
|
974
|
-
}, { summary: `✓ Token refreshed for ${instanceURL}` });
|
|
975
|
-
}),
|
|
1112
|
+
describe: 'Refresh OAuth credentials or renew a browser session',
|
|
1113
|
+
builder: sub => sub
|
|
1114
|
+
.option('browser', { type: 'boolean', describe: 'Explicitly open a browser to renew the session' })
|
|
1115
|
+
.option('browser-name', { type: 'string', describe: 'Browser to launch: chromium, chrome, or edge' })
|
|
1116
|
+
.option('browser-url', { type: 'string', describe: 'Advanced: local browser debugging URL (requires --browser)' })
|
|
1117
|
+
.option('headers', { type: 'string', describe: 'Captured browser request headers or cURL command' }),
|
|
1118
|
+
handler: wrap(refreshProfile),
|
|
976
1119
|
})
|
|
977
1120
|
.command({
|
|
978
1121
|
command: 'switch [name]',
|
|
979
|
-
describe: '
|
|
1122
|
+
describe: 'Choose the profile used by default',
|
|
980
1123
|
builder: (sub) => sub
|
|
981
1124
|
.positional('name', {
|
|
982
1125
|
describe: 'Profile name to activate',
|
|
983
1126
|
type: 'string',
|
|
984
1127
|
}),
|
|
985
|
-
handler: wrap(
|
|
986
|
-
let name = argv.name;
|
|
987
|
-
if (!name) {
|
|
988
|
-
const profileNames = Object.keys(app.config.profiles || {});
|
|
989
|
-
if (profileNames.length === 0) {
|
|
990
|
-
throw errUsage('No profiles configured. Run "jsn auth login" first.');
|
|
991
|
-
}
|
|
992
|
-
if (profileNames.length === 1) {
|
|
993
|
-
name = profileNames[0];
|
|
994
|
-
} else if (canUsePicker(app, argv)) {
|
|
995
|
-
const { search } = await import('@inquirer/prompts');
|
|
996
|
-
const choices = profileNames.map(profileName => ({
|
|
997
|
-
name: `${profileName} — ${app.config.profiles[profileName].instance_url}`,
|
|
998
|
-
value: profileName,
|
|
999
|
-
}));
|
|
1000
|
-
const selected = await search({
|
|
1001
|
-
message: 'Select a profile:',
|
|
1002
|
-
source: (input) => {
|
|
1003
|
-
const filter = (input || '').toLowerCase();
|
|
1004
|
-
return choices.filter(c => c.name.toLowerCase().includes(filter));
|
|
1005
|
-
},
|
|
1006
|
-
});
|
|
1007
|
-
name = selected;
|
|
1008
|
-
} else {
|
|
1009
|
-
// Never silently switch the active profile for a
|
|
1010
|
-
// non-interactive caller: picking one for them is a
|
|
1011
|
-
// config write they did not ask for.
|
|
1012
|
-
throw errUsageHint(
|
|
1013
|
-
`Which profile? ${profileNames.length} profiles are configured: ${profileNames.join(', ')}`,
|
|
1014
|
-
'Pass the profile name: jsn auth switch <name>'
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
await setActiveProfile(app.config, name);
|
|
1019
|
-
app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
|
|
1020
|
-
}),
|
|
1128
|
+
handler: wrap(switchProfile),
|
|
1021
1129
|
})
|
|
1022
1130
|
.command({
|
|
1023
1131
|
command: 'modify [name]',
|
|
1024
|
-
describe: '
|
|
1132
|
+
describe: 'Change profile settings (read-only, counts, delete confirmations, domain)',
|
|
1025
1133
|
builder: (sub) => sub
|
|
1134
|
+
.option('read-only', { type: 'boolean', describe: 'Set read-only access (use --no-read-only to allow writes)' })
|
|
1135
|
+
.option('include-counts', { type: 'boolean', describe: 'Set result counts (use --no-include-counts to disable)' })
|
|
1136
|
+
.option('confirm-deletes', { type: 'boolean', describe: 'Set delete confirmations (use --no-confirm-deletes to disable)' })
|
|
1137
|
+
.option('skip-confirmations', { type: 'boolean', describe: 'Compatibility flag: disable delete confirmations' })
|
|
1026
1138
|
.positional('name', {
|
|
1027
1139
|
describe: 'Profile name to modify',
|
|
1028
1140
|
type: 'string',
|
|
@@ -1043,25 +1155,26 @@ Examples:
|
|
|
1043
1155
|
describe: 'Profile name to delete',
|
|
1044
1156
|
type: 'string',
|
|
1045
1157
|
}),
|
|
1046
|
-
handler: wrap(
|
|
1047
|
-
let name = argv.name;
|
|
1048
|
-
if (!name) {
|
|
1049
|
-
name = await pickProfile(app, 'Remove which instance?');
|
|
1050
|
-
}
|
|
1051
|
-
await removeProfile(app, name);
|
|
1052
|
-
}),
|
|
1158
|
+
handler: wrap(removeConnection),
|
|
1053
1159
|
});
|
|
1054
1160
|
},
|
|
1055
|
-
handler: () => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1161
|
+
handler: wrap(async (_argv, app) => {
|
|
1162
|
+
const commands = [
|
|
1163
|
+
{ name: 'login', description: 'Sign in or add a connection' },
|
|
1164
|
+
{ name: 'status', description: 'Show authentication and connection status' },
|
|
1165
|
+
{ name: 'refresh', description: 'Refresh OAuth credentials or renew a browser session' },
|
|
1166
|
+
{ name: 'switch', description: 'Choose the default profile' },
|
|
1167
|
+
{ name: 'modify', description: 'Change connection settings' },
|
|
1168
|
+
{ name: 'logout', description: 'Sign out and keep the profile' },
|
|
1169
|
+
{ name: 'remove', description: 'Remove a connection and its credentials' },
|
|
1170
|
+
];
|
|
1171
|
+
app.ok({
|
|
1172
|
+
commands: commands.map(command => command.name),
|
|
1173
|
+
_formatted: formatCommandList({ title: 'Manage ServiceNow authentication and connection settings', commands }),
|
|
1174
|
+
}, {
|
|
1175
|
+
summary: 'Manage ServiceNow authentication and connection settings',
|
|
1176
|
+
breadcrumbs: [{ action: 'Guided setup', cmd: 'jsn setup', description: 'Interactive login and connection management' }],
|
|
1177
|
+
});
|
|
1178
|
+
}),
|
|
1066
1179
|
};
|
|
1067
1180
|
}
|