@jacebenson/jsn 4.0.1 → 4.0.3
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 +29 -7
- package/package.json +3 -4
- package/src/commands/auth.js +113 -46
- package/src/commands/dev/flows.js +51 -0
- package/src/commands/records.js +1 -1
- package/src/commands/setup.js +53 -17
- package/src/help.js +2 -1
- package/suppress-warnings.cjs +0 -13
package/README.md
CHANGED
|
@@ -14,11 +14,27 @@ Node.js 18+. macOS, Linux, Windows.
|
|
|
14
14
|
|
|
15
15
|
The install also copies an AI agent skill file to `~/.agents/skills/servicenow/SKILL.md`.
|
|
16
16
|
|
|
17
|
+
### Install from GitHub (test unreleased builds)
|
|
18
|
+
|
|
19
|
+
To test a branch or the freshly merged `main` before it's published to npm:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g github:jacebenson/jsn#main # merged but not yet tagged
|
|
23
|
+
npm install -g github:jacebenson/jsn#<branch> # any PR/feature branch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Go back to the released version with:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @jacebenson/jsn
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Note:** git installs build `better-sqlite3` (docs search) from source, so the machine needs build tools (Python, make, a C++ compiler).
|
|
33
|
+
|
|
17
34
|
## Quick Start
|
|
18
35
|
|
|
19
36
|
```bash
|
|
20
|
-
jsn setup # Interactive
|
|
21
|
-
jsn auth login https://dev12345.service-now.com
|
|
37
|
+
jsn setup # Interactive: add, switch, remove, modify instances
|
|
22
38
|
jsn incidents list --query "priority=1" # Critical incidents
|
|
23
39
|
```
|
|
24
40
|
|
|
@@ -64,17 +80,23 @@ Run `jsn` for the full grouped list. Commands are organized by ServiceNow domain
|
|
|
64
80
|
|
|
65
81
|
Every command supports `--json`, `--query`, and `--help`.
|
|
66
82
|
|
|
67
|
-
##
|
|
83
|
+
## Instances
|
|
68
84
|
|
|
69
85
|
Switch between instances without re-authenticating.
|
|
70
86
|
|
|
71
87
|
```bash
|
|
72
|
-
jsn
|
|
73
|
-
jsn auth status
|
|
74
|
-
jsn auth switch dev12345
|
|
88
|
+
jsn setup # Interactive: add, switch, remove, or modify instances
|
|
89
|
+
jsn auth status # Dashboard — auth state, read-only 🔒, skip-confirmations ⚡
|
|
75
90
|
```
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
`jsn setup` is the human front door — one command, interactive menu. For scripts and CI, use the atomic commands:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
jsn auth login https://dev12345.service-now.com # Add + authenticate (scripted)
|
|
96
|
+
jsn auth refresh # Manually refresh the OAuth token
|
|
97
|
+
jsn auth switch dev12345 # Flip the active profile (scripted)
|
|
98
|
+
jsn auth modify dev12345 # Toggle read-only / skip confirmations
|
|
99
|
+
```
|
|
78
100
|
|
|
79
101
|
## Output Formats
|
|
80
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "src/cli.js",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "node --experimental-test-module-mocks --test $(find test -name '*.test.js')",
|
|
12
12
|
"lint": "npx eslint src/ bin/ test/",
|
|
13
|
-
"start": "
|
|
13
|
+
"start": "node bin/jsn.js",
|
|
14
14
|
"tag-reminder": "node scripts/tag-reminder.js",
|
|
15
15
|
"release": "node scripts/release.js"
|
|
16
16
|
},
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"bin/jsn.js",
|
|
45
|
-
"suppress-warnings.cjs",
|
|
46
45
|
"src/",
|
|
47
46
|
"skills/",
|
|
48
47
|
"scripts/",
|
package/src/commands/auth.js
CHANGED
|
@@ -23,6 +23,37 @@ export function resolveInstanceArg(arg, cfg) {
|
|
|
23
23
|
return normalizeInstanceURL(`${trimmed}.service-now.com`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Pick a profile by name — interactive search picker when ambiguous,
|
|
28
|
+
* auto-selects when there's exactly one, throws when none configured.
|
|
29
|
+
*/
|
|
30
|
+
export async function pickProfile(app, message) {
|
|
31
|
+
const { search } = await import('@inquirer/prompts');
|
|
32
|
+
const profileNames = Object.keys(app.config.profiles || {});
|
|
33
|
+
if (profileNames.length === 0) throw errUsage('No profiles configured. Run "jsn setup" first.');
|
|
34
|
+
if (profileNames.length === 1 || !app.isInteractive()) return profileNames[0];
|
|
35
|
+
const choices = profileNames.map(name => ({
|
|
36
|
+
name: `${name} — ${app.config.profiles[name].instance_url}`,
|
|
37
|
+
value: name,
|
|
38
|
+
}));
|
|
39
|
+
return search({
|
|
40
|
+
message,
|
|
41
|
+
source: (input) => {
|
|
42
|
+
const filter = (input || '').toLowerCase();
|
|
43
|
+
return choices.filter(c => c.name.toLowerCase().includes(filter));
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the instance for the wizard: only an EXPLICIT source (env var,
|
|
50
|
+
* --instance flag/override) pre-fills. The config's default profile must
|
|
51
|
+
* NOT — the wizard adds an instance, so it always asks otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveWizardInstance(app, argv = {}) {
|
|
54
|
+
return process.env.SERVICENOW_INSTANCE_URL || app._overrideInstance || argv.instance || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
/**
|
|
27
58
|
* Interactive first-run wizard (formerly `jsn setup`).
|
|
28
59
|
*
|
|
@@ -39,8 +70,15 @@ export async function loginWizard(app, argv = {}) {
|
|
|
39
70
|
console.log('Welcome to JSN - ServiceNow CLI');
|
|
40
71
|
console.log();
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
// Ask for the instance URL unless one was explicitly provided (env var,
|
|
74
|
+
// --instance flag/override). NEVER pre-fill from the config's default
|
|
75
|
+
// profile — this wizard adds an instance, so it must always ask.
|
|
76
|
+
// (Regression: setup → Add skipped the URL ask when a default profile
|
|
77
|
+
// existed, silently re-targeting the default instance.)
|
|
78
|
+
let instance = resolveWizardInstance(app, argv);
|
|
79
|
+
if (instance) {
|
|
80
|
+
instance = normalizeInstanceURL(instance);
|
|
81
|
+
} else {
|
|
44
82
|
instance = await ask('ServiceNow instance URL (e.g., dev12345.service-now.com): ');
|
|
45
83
|
instance = normalizeInstanceURL(instance);
|
|
46
84
|
}
|
|
@@ -165,6 +203,63 @@ async function pickLoginTarget(app) {
|
|
|
165
203
|
return { addNew: false, name: selected };
|
|
166
204
|
}
|
|
167
205
|
|
|
206
|
+
/** Per-profile configurable flags (extensible — any boolean on the profile). */
|
|
207
|
+
const PROFILE_FLAGS = [
|
|
208
|
+
{ name: 'Read-only (blocks mutation commands)', value: 'read_only', icon: '🔒' },
|
|
209
|
+
{ name: 'Skip confirmations (deletes run without prompting)', value: 'skip_confirmations', icon: '⚡' },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Toggle a per-profile configuration flag (read_only / skip_confirmations).
|
|
214
|
+
* Interactive flag picker when no flag passed; testable via argv.flag.
|
|
215
|
+
*/
|
|
216
|
+
export async function modifyProfile(app, argv = {}) {
|
|
217
|
+
const name = argv.name || await pickProfile(app, 'Modify which instance?');
|
|
218
|
+
const profile = app.config.profiles?.[name];
|
|
219
|
+
if (!profile) throw new Error(`Profile not found: ${name}`);
|
|
220
|
+
|
|
221
|
+
let flag = argv.flag;
|
|
222
|
+
if (!flag) {
|
|
223
|
+
const { select } = await import('@inquirer/prompts');
|
|
224
|
+
flag = await select({
|
|
225
|
+
message: `Modify ${name} (${profile.instance_url})`,
|
|
226
|
+
choices: PROFILE_FLAGS.map(f => ({
|
|
227
|
+
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
228
|
+
value: f.value,
|
|
229
|
+
})),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
profile[flag] = !profile[flag];
|
|
234
|
+
await saveConfig(app.config);
|
|
235
|
+
app.ok({ profile: name, [flag]: profile[flag] },
|
|
236
|
+
{ summary: `${flag} is now ${profile[flag] ? 'ON' : 'off'} for ${name}` });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Delete an instance entry and its stored credentials (shared by the
|
|
241
|
+
* `auth remove` subcommand and the `jsn setup` hub).
|
|
242
|
+
*/
|
|
243
|
+
export async function removeProfile(app, name) {
|
|
244
|
+
if (!app.config.profiles[name]) {
|
|
245
|
+
throw new Error(`Profile not found: ${name}`);
|
|
246
|
+
}
|
|
247
|
+
const instance = app.config.profiles[name].instance_url;
|
|
248
|
+
delete app.config.profiles[name];
|
|
249
|
+
if (app.config.defaultProfile === name) app.config.defaultProfile = '';
|
|
250
|
+
if (app.config.activeProfile === name) app.config.activeProfile = '';
|
|
251
|
+
saveConfig(app.config);
|
|
252
|
+
|
|
253
|
+
// Only clear credentials if no other profile uses this instance URL
|
|
254
|
+
const stillInUse = instance && Object.values(app.config.profiles || {})
|
|
255
|
+
.some(p => p.instance_url === instance);
|
|
256
|
+
if (instance && !stillInUse) {
|
|
257
|
+
app.auth.logout(instance);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
|
|
261
|
+
}
|
|
262
|
+
|
|
168
263
|
export function authCmd(wrap) {
|
|
169
264
|
return {
|
|
170
265
|
command: 'auth [subcommand]',
|
|
@@ -507,6 +602,18 @@ Examples:
|
|
|
507
602
|
app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
|
|
508
603
|
}),
|
|
509
604
|
})
|
|
605
|
+
.command({
|
|
606
|
+
command: 'modify [name]',
|
|
607
|
+
describe: 'Toggle instance configuration (read-only, skip confirmations)',
|
|
608
|
+
builder: (sub) => sub
|
|
609
|
+
.positional('name', {
|
|
610
|
+
describe: 'Profile name to modify',
|
|
611
|
+
type: 'string',
|
|
612
|
+
}),
|
|
613
|
+
handler: wrap(async (argv, app) => {
|
|
614
|
+
await modifyProfile(app, argv);
|
|
615
|
+
}),
|
|
616
|
+
})
|
|
510
617
|
.command({
|
|
511
618
|
command: 'remove [name]',
|
|
512
619
|
describe: 'Delete an instance entry and its stored credentials',
|
|
@@ -518,45 +625,9 @@ Examples:
|
|
|
518
625
|
handler: wrap(async (argv, app) => {
|
|
519
626
|
let name = argv.name;
|
|
520
627
|
if (!name) {
|
|
521
|
-
|
|
522
|
-
if (profileNames.length === 0) {
|
|
523
|
-
throw errUsage('No profiles configured. Nothing to remove.');
|
|
524
|
-
}
|
|
525
|
-
if (profileNames.length > 1 && app.isInteractive()) {
|
|
526
|
-
const { search } = await import('@inquirer/prompts');
|
|
527
|
-
const choices = profileNames.map(profileName => ({
|
|
528
|
-
name: `${profileName} — ${app.config.profiles[profileName].instance_url}`,
|
|
529
|
-
value: profileName,
|
|
530
|
-
}));
|
|
531
|
-
const selected = await search({
|
|
532
|
-
message: 'Remove which profile?',
|
|
533
|
-
source: (input) => {
|
|
534
|
-
const filter = (input || '').toLowerCase();
|
|
535
|
-
return choices.filter(c => c.name.toLowerCase().includes(filter));
|
|
536
|
-
},
|
|
537
|
-
});
|
|
538
|
-
name = selected;
|
|
539
|
-
} else {
|
|
540
|
-
name = profileNames[0];
|
|
541
|
-
}
|
|
628
|
+
name = await pickProfile(app, 'Remove which instance?');
|
|
542
629
|
}
|
|
543
|
-
|
|
544
|
-
throw new Error(`Profile not found: ${name}`);
|
|
545
|
-
}
|
|
546
|
-
const instance = app.config.profiles[name].instance_url;
|
|
547
|
-
delete app.config.profiles[name];
|
|
548
|
-
if (app.config.defaultProfile === name) app.config.defaultProfile = '';
|
|
549
|
-
if (app.config.activeProfile === name) app.config.activeProfile = '';
|
|
550
|
-
saveConfig(app.config);
|
|
551
|
-
|
|
552
|
-
// Only clear credentials if no other profile uses this instance URL
|
|
553
|
-
const stillInUse = instance && Object.values(app.config.profiles || {})
|
|
554
|
-
.some(p => p.instance_url === instance);
|
|
555
|
-
if (instance && !stillInUse) {
|
|
556
|
-
app.auth.logout(instance);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
|
|
630
|
+
await removeProfile(app, name);
|
|
560
631
|
}),
|
|
561
632
|
});
|
|
562
633
|
},
|
|
@@ -565,15 +636,11 @@ Examples:
|
|
|
565
636
|
console.log('');
|
|
566
637
|
console.log('Available subcommands:');
|
|
567
638
|
console.log(' login <url|name> Log in / add an instance (picker when run bare)');
|
|
568
|
-
console.log(' status Show auth status for all profiles');
|
|
569
|
-
console.log(' switch <name> Switch the active profile');
|
|
570
639
|
console.log(' refresh Refresh the access token');
|
|
571
|
-
console.log('
|
|
572
|
-
console.log(' remove <name> Delete an instance entry and its credentials');
|
|
640
|
+
console.log(' status Show auth status for all profiles');
|
|
573
641
|
console.log('');
|
|
642
|
+
console.log('Tip: "jsn setup" is the interactive manager — add, switch, remove, or modify instances.');
|
|
574
643
|
console.log('Run "jsn auth <command> --help" for details.');
|
|
575
|
-
console.log('');
|
|
576
|
-
console.log('Tip: "jsn auth login" is the only command you need to add an instance.');
|
|
577
644
|
},
|
|
578
645
|
};
|
|
579
646
|
}
|
|
@@ -307,6 +307,11 @@ async function formatFlowStructure(inspection, ctx) {
|
|
|
307
307
|
if (!ctx.catalogVarNames) ctx.catalogVarNames = new Map();
|
|
308
308
|
for (const [k, v] of names) ctx.catalogVarNames.set(k, v);
|
|
309
309
|
}
|
|
310
|
+
const labelCache = buildLabelCache(inspection.payload);
|
|
311
|
+
if (labelCache.size > 0) {
|
|
312
|
+
if (!ctx.labelCache) ctx.labelCache = new Map();
|
|
313
|
+
for (const [k, v] of labelCache) ctx.labelCache.set(k, v);
|
|
314
|
+
}
|
|
310
315
|
const payload = inspection.payload;
|
|
311
316
|
if (Object.keys(payload).length > 0) {
|
|
312
317
|
return formatFlowStructureFromPayload(payload, ctx);
|
|
@@ -314,6 +319,48 @@ async function formatFlowStructure(inspection, ctx) {
|
|
|
314
319
|
return formatFlowStructureFallback(inspection);
|
|
315
320
|
}
|
|
316
321
|
|
|
322
|
+
/**
|
|
323
|
+
* The ProcessFlow payload carries a label cache (also serialized as
|
|
324
|
+
* labelCacheAsJsonString) mapping data-pill names like "<step-uid>.<var>"
|
|
325
|
+
* to the human labels the Flow Designer UI shows, e.g.
|
|
326
|
+
* "1 - Get Catalog Variables➛session_title". Build a Map so guid-prefixed
|
|
327
|
+
* pills can be rendered readably.
|
|
328
|
+
*/
|
|
329
|
+
function buildLabelCache(payload) {
|
|
330
|
+
const cache = new Map();
|
|
331
|
+
if (!payload || typeof payload !== 'object') return cache;
|
|
332
|
+
let entries = payload.label_cache;
|
|
333
|
+
if (!Array.isArray(entries)) {
|
|
334
|
+
try {
|
|
335
|
+
entries = JSON.parse(payload.labelCacheAsJsonString || '[]');
|
|
336
|
+
} catch {
|
|
337
|
+
entries = [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const e of entries) {
|
|
341
|
+
if (!e || typeof e !== 'object') continue;
|
|
342
|
+
const name = getStringField(e, 'name');
|
|
343
|
+
const label = getStringField(e, 'label');
|
|
344
|
+
if (name && label) cache.set(name, label);
|
|
345
|
+
}
|
|
346
|
+
return cache;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const GUID_PILL_RE = /\{\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.([^}]+)\}\}/g;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Replace opaque guid-prefixed pills ({{<32-hex-guid>.<var>}}) with their
|
|
353
|
+
* human label from the payload's label cache. Readable step refs like
|
|
354
|
+
* {{Created_1.table_name}} are NOT matched and stay raw.
|
|
355
|
+
*/
|
|
356
|
+
function resolveGuidPills(value, labelCache) {
|
|
357
|
+
if (!labelCache || labelCache.size === 0 || !value) return value;
|
|
358
|
+
return String(value).replace(GUID_PILL_RE, (m, guid, name) => {
|
|
359
|
+
const label = labelCache.get(`${guid}.${name}`);
|
|
360
|
+
return label ? `{{${label}}}` : m;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
317
364
|
/**
|
|
318
365
|
* ServiceNow serializes catalog variable selections in action inputs as
|
|
319
366
|
* comma-separated "sys_id:table" pairs (e.g. Get Catalog Variables / Create
|
|
@@ -657,6 +704,10 @@ export function formatActionStep(stepNum, pad, action, ctx) {
|
|
|
657
704
|
const resolvedCatalog = resolveCatalogVarValue(inputValue, ctx?.catalogVarNames);
|
|
658
705
|
if (resolvedCatalog) inputValue = resolvedCatalog;
|
|
659
706
|
|
|
707
|
+
// Opaque guid-prefixed pills ({{<32-hex>.<var>}}) get their human
|
|
708
|
+
// label from the payload's label cache. Readable step refs stay raw.
|
|
709
|
+
inputValue = resolveGuidPills(inputValue, ctx?.labelCache);
|
|
710
|
+
|
|
660
711
|
let label = inputName;
|
|
661
712
|
if (raw.parameter && typeof raw.parameter === 'object') {
|
|
662
713
|
label = firstNonEmpty(getStringField(raw.parameter, 'label'), label);
|
package/src/commands/records.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete } from '../helpers.js';
|
|
1
|
+
import { formatRecordForDisplay, buildQuerySuffix, parseDataArg, getStringField, interactiveList, resolveFieldsParam, checkDerivedFields, confirmDelete, assertSafeExactMatch } from '../helpers.js';
|
|
2
2
|
|
|
3
3
|
const tableDefaultColumns = {
|
|
4
4
|
incident: ['number', 'short_description', 'priority', 'state', 'assigned_to'],
|
package/src/commands/setup.js
CHANGED
|
@@ -1,25 +1,61 @@
|
|
|
1
|
-
import { loginWizard } from './auth.js';
|
|
1
|
+
import { loginWizard, modifyProfile, removeProfile, pickProfile } from './auth.js';
|
|
2
|
+
import { setActiveProfile } from '../config.js';
|
|
3
|
+
import { errUsage } from '../errors.js';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** Interactive hub: what do you want to do? Returns 'add' | 'switch' | 'remove' | 'modify'. */
|
|
6
|
+
export async function authHubMenu() {
|
|
7
|
+
const { select } = await import('@inquirer/prompts');
|
|
8
|
+
return select({
|
|
9
|
+
message: 'What would you like to do?',
|
|
10
|
+
choices: [
|
|
11
|
+
{ name: 'Add a new instance', value: 'add' },
|
|
12
|
+
{ name: 'Switch to a different instance', value: 'switch' },
|
|
13
|
+
{ name: 'Remove an instance', value: 'remove' },
|
|
14
|
+
{ name: 'Modify an instance', value: 'modify' },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dispatch a hub action to the shared operation. Kept separate from the
|
|
21
|
+
* handler so routing is testable without invoking inquirer prompts.
|
|
22
|
+
*/
|
|
23
|
+
export async function dispatchSetupAction(app, argv, action) {
|
|
24
|
+
if (action === 'add') {
|
|
25
|
+
const result = await loginWizard(app, argv);
|
|
26
|
+
app.ok({ setup: true, ...result }, { summary: 'Setup complete' });
|
|
27
|
+
} else if (action === 'switch') {
|
|
28
|
+
const name = await pickProfile(app, 'Switch to which instance?');
|
|
29
|
+
await setActiveProfile(app.config, name);
|
|
30
|
+
app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
|
|
31
|
+
} else if (action === 'remove') {
|
|
32
|
+
const name = await pickProfile(app, 'Remove which instance?');
|
|
33
|
+
await removeProfile(app, name);
|
|
34
|
+
} else { // modify
|
|
35
|
+
await modifyProfile(app, argv);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `jsn setup` is the interactive front door for managing instances.
|
|
40
|
+
// The granular, scriptable surface lives under `jsn auth` — this command
|
|
41
|
+
// dispatches to the SAME functions (loginWizard, setActiveProfile,
|
|
42
|
+
// removeProfile, modifyProfile) so the two surfaces can never drift.
|
|
6
43
|
export function setupCmd(wrap) {
|
|
7
44
|
return {
|
|
8
45
|
command: 'setup',
|
|
9
|
-
describe: '
|
|
10
|
-
hidden: true, // legacy alias for `jsn auth login` — works but hidden from help
|
|
11
|
-
builder: (yargs) => {
|
|
12
|
-
return yargs
|
|
13
|
-
.option('read-only', {
|
|
14
|
-
describe: 'Mark profile as read-only (blocks mutation commands)',
|
|
15
|
-
type: 'boolean',
|
|
16
|
-
default: false,
|
|
17
|
-
});
|
|
18
|
-
},
|
|
46
|
+
describe: 'Set up and manage your ServiceNow instances (interactive)',
|
|
19
47
|
handler: wrap(async (argv, app) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
if (!app.isInteractive()) {
|
|
49
|
+
throw errUsage('"jsn setup" is interactive. Use the specific commands:\n jsn auth login <instance>\n jsn auth refresh\n jsn auth status');
|
|
50
|
+
}
|
|
51
|
+
const profileNames = Object.keys(app.config.profiles || {});
|
|
52
|
+
if (profileNames.length === 0) {
|
|
53
|
+
const result = await loginWizard(app, argv);
|
|
54
|
+
app.ok({ setup: true, ...result }, { summary: 'Setup complete' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const action = await authHubMenu();
|
|
58
|
+
await dispatchSetupAction(app, argv, action);
|
|
23
59
|
}),
|
|
24
60
|
};
|
|
25
61
|
}
|
package/src/help.js
CHANGED
|
@@ -107,7 +107,8 @@ const DATA_COMMANDS = [
|
|
|
107
107
|
];
|
|
108
108
|
|
|
109
109
|
const CONFIG_COMMANDS = [
|
|
110
|
-
{ name: '
|
|
110
|
+
{ name: 'setup', desc: 'Set up and manage instances (interactive: add, switch, remove, modify)' },
|
|
111
|
+
{ name: 'auth', desc: 'Authentication (login, refresh, status)' },
|
|
111
112
|
{ name: 'updatesets', desc: 'Update sets (set, create, export)' },
|
|
112
113
|
{ name: 'scopes', desc: 'Application scopes' },
|
|
113
114
|
];
|
package/suppress-warnings.cjs
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Suppresses the node:sqlite ExperimentalWarning on Node 22.x.
|
|
2
|
-
// Loaded via NODE_OPTIONS=--require before the main ESM entry point.
|
|
3
|
-
|
|
4
|
-
const original = process.emitWarning;
|
|
5
|
-
process.emitWarning = function emitWarning(warning, ...args) {
|
|
6
|
-
const message = typeof warning === 'string'
|
|
7
|
-
? warning
|
|
8
|
-
: (warning?.message || '');
|
|
9
|
-
if (message.includes('SQLite')) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
return original.apply(this, [warning, ...args]);
|
|
13
|
-
};
|