@jacebenson/jsn 4.0.2 → 4.0.4
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/app.js +25 -3
- package/src/cli.js +3 -1
- package/src/commands/auth.js +222 -46
- package/src/commands/dev/domains.js +292 -0
- package/src/commands/dev/scopes.js +88 -36
- package/src/commands/dev/updatesets.js +297 -106
- package/src/commands/dev.js +16 -1
- package/src/commands/records.js +4 -3
- package/src/commands/setup.js +53 -17
- package/src/context.js +52 -5
- package/src/help.js +18 -3
- package/src/mutations.js +10 -1
- package/src/sdk.js +4 -0
- 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.4",
|
|
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/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { SDKClient } from './sdk.js';
|
|
5
5
|
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
|
|
6
|
-
import { getEffectiveInstance, normalizeInstanceURL } from './config.js';
|
|
6
|
+
import { getEffectiveInstance, normalizeInstanceURL, saveConfig } from './config.js';
|
|
7
7
|
import { extractProfileName } from './helpers.js';
|
|
8
8
|
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
9
9
|
import { errUsage, errAuth } from './errors.js';
|
|
@@ -20,7 +20,9 @@ export class App {
|
|
|
20
20
|
|
|
21
21
|
const instance = getEffectiveInstance(cfg);
|
|
22
22
|
if (instance) {
|
|
23
|
-
this.sdk = new SDKClient(instance, this.auth
|
|
23
|
+
this.sdk = new SDKClient(instance, this.auth, {
|
|
24
|
+
domain: this._profileDomain(cfg),
|
|
25
|
+
});
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
this.context = {
|
|
@@ -202,9 +204,29 @@ export class App {
|
|
|
202
204
|
const normalizedUrl = normalizeInstanceURL(url);
|
|
203
205
|
if (normalizedUrl) {
|
|
204
206
|
this._overrideInstance = normalizedUrl;
|
|
205
|
-
this.sdk = new SDKClient(normalizedUrl, this.auth
|
|
207
|
+
this.sdk = new SDKClient(normalizedUrl, this.auth, {
|
|
208
|
+
domain: this._profileDomain(this.config),
|
|
209
|
+
});
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
|
|
213
|
+
/** Read the configured domain for the active profile (empty = no scoping). */
|
|
214
|
+
_profileDomain(cfg) {
|
|
215
|
+
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
216
|
+
if (name && cfg.profiles[name]) {
|
|
217
|
+
return cfg.profiles[name].domain || '';
|
|
218
|
+
}
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Set (or clear with '') the domain for the active profile. */
|
|
223
|
+
setDomain(domain) {
|
|
224
|
+
const name = this.config.activeProfile || this.config.defaultProfile;
|
|
225
|
+
if (!name) throw errUsage('No active profile to set a domain on');
|
|
226
|
+
if (!this.config.profiles[name]) this.config.profiles[name] = {};
|
|
227
|
+
this.config.profiles[name].domain = domain || '';
|
|
228
|
+
return saveConfig(this.config);
|
|
229
|
+
}
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
function resolveFormat(fmt) {
|
package/src/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ import { formsCmd } from './commands/dev/forms.js';
|
|
|
53
53
|
import { listsCmd } from './commands/dev/lists.js';
|
|
54
54
|
import { updateSetsCmd } from './commands/dev/updatesets.js';
|
|
55
55
|
import { scopesCmd } from './commands/dev/scopes.js';
|
|
56
|
+
import { domainsCmd } from './commands/dev/domains.js';
|
|
56
57
|
import { evalCmd } from './commands/dev/eval.js';
|
|
57
58
|
import { restCmd } from './commands/dev/rest.js';
|
|
58
59
|
import { logsCmd } from './commands/dev/logs.js';
|
|
@@ -287,6 +288,7 @@ export function buildCLI() {
|
|
|
287
288
|
.command(authCmd(wrap))
|
|
288
289
|
.command(updateSetsCmd(wrap))
|
|
289
290
|
.command(scopesCmd(wrap))
|
|
291
|
+
.command(domainsCmd(wrap))
|
|
290
292
|
// CORE
|
|
291
293
|
.command(incidentsCmd(wrap))
|
|
292
294
|
.command(changesCmd(wrap))
|
|
@@ -388,7 +390,7 @@ export function buildCLI() {
|
|
|
388
390
|
}
|
|
389
391
|
process.exit(0);
|
|
390
392
|
}
|
|
391
|
-
process.stdout.write(renderHelp() + '\n');
|
|
393
|
+
process.stdout.write(renderHelp(loadConfig()) + '\n');
|
|
392
394
|
process.exit(0);
|
|
393
395
|
}
|
|
394
396
|
process.stderr.write(msg + '\n');
|
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
|
}
|
|
@@ -113,6 +151,36 @@ export async function loginWizard(app, argv = {}) {
|
|
|
113
151
|
}
|
|
114
152
|
console.log('Login successful!');
|
|
115
153
|
loggedIn = true;
|
|
154
|
+
|
|
155
|
+
// Probe domain separation capability once and stamp the profile, so
|
|
156
|
+
// `jsn domains` (and its help entry) only appear on instances with it.
|
|
157
|
+
try {
|
|
158
|
+
const { SDKClient } = await import('../sdk.js');
|
|
159
|
+
const domainsMod = await import('./dev/domains.js');
|
|
160
|
+
const sdk = new SDKClient(instance, app.auth);
|
|
161
|
+
profile.domain_separation = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
162
|
+
await setProfile(app.config, profileName, profile);
|
|
163
|
+
|
|
164
|
+
// If domain separation is installed, offer to pick a default domain
|
|
165
|
+
// for the profile — every request will be scoped to it via X-Now-Domain.
|
|
166
|
+
if (profile.domain_separation) {
|
|
167
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
168
|
+
const setDomainNow = await confirm({
|
|
169
|
+
message: 'Domain separation detected. Configure a domain for this instance?',
|
|
170
|
+
default: false,
|
|
171
|
+
});
|
|
172
|
+
if (setDomainNow) {
|
|
173
|
+
const domainSysID = await domainsMod.pickDomain({ ...app, sdk });
|
|
174
|
+
if (domainSysID) {
|
|
175
|
+
profile.domain = domainSysID;
|
|
176
|
+
await setProfile(app.config, profileName, profile);
|
|
177
|
+
console.log('Domain configured.');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Non-fatal — capability unknown; domains command falls back to probing
|
|
183
|
+
}
|
|
116
184
|
}
|
|
117
185
|
} else {
|
|
118
186
|
rl.close();
|
|
@@ -165,6 +233,121 @@ async function pickLoginTarget(app) {
|
|
|
165
233
|
return { addNew: false, name: selected };
|
|
166
234
|
}
|
|
167
235
|
|
|
236
|
+
/** Per-profile configurable flags (extensible — any boolean on the profile). */
|
|
237
|
+
const PROFILE_FLAGS = [
|
|
238
|
+
{ name: 'Read-only (blocks mutation commands)', value: 'read_only', icon: '🔒' },
|
|
239
|
+
{ name: 'Skip confirmations (deletes run without prompting)', value: 'skip_confirmations', icon: '⚡' },
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Modify a profile: toggle a boolean flag, or set/clear the domain.
|
|
244
|
+
* Interactive picker when no flag passed; testable via argv.flag/argv.domain.
|
|
245
|
+
*/
|
|
246
|
+
export async function modifyProfile(app, argv = {}) {
|
|
247
|
+
const name = argv.name || await pickProfile(app, 'Modify which instance?');
|
|
248
|
+
const profile = app.config.profiles?.[name];
|
|
249
|
+
if (!profile) throw new Error(`Profile not found: ${name}`);
|
|
250
|
+
|
|
251
|
+
// Domain setting path: --domain <name|sys_id|clear>
|
|
252
|
+
if (argv.domain !== undefined) {
|
|
253
|
+
if (!profile.domain_separation) {
|
|
254
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url} — no domain to configure.`);
|
|
255
|
+
}
|
|
256
|
+
if (argv.domain === 'clear' || argv.domain === 'none' || argv.domain === '') {
|
|
257
|
+
profile.domain = '';
|
|
258
|
+
await saveConfig(app.config);
|
|
259
|
+
app.ok({ profile: name, domain: '' }, { summary: `Domain cleared for ${name}` });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
263
|
+
const { SDKClient } = await import('../sdk.js');
|
|
264
|
+
const sdk = new SDKClient(profile.instance_url, app.auth);
|
|
265
|
+
if (!(await isDomainSeparationInstalled({ sdk }))) {
|
|
266
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
267
|
+
}
|
|
268
|
+
const { resolveDomainSysId } = await import('./dev/domains.js');
|
|
269
|
+
const sysID = await resolveDomainSysId({ sdk }, argv.domain);
|
|
270
|
+
profile.domain = sysID;
|
|
271
|
+
await saveConfig(app.config);
|
|
272
|
+
app.ok({ profile: name, domain: sysID }, { summary: `Domain set for ${name}` });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let flag = argv.flag;
|
|
277
|
+
if (!flag) {
|
|
278
|
+
const { select } = await import('@inquirer/prompts');
|
|
279
|
+
const choices = PROFILE_FLAGS.map(f => ({
|
|
280
|
+
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
281
|
+
value: f.value,
|
|
282
|
+
}));
|
|
283
|
+
if (profile.domain_separation) {
|
|
284
|
+
choices.push({
|
|
285
|
+
name: `🌐 Domain: ${profile.domain ? 'set' : 'not set'}`,
|
|
286
|
+
value: 'domain',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const { SDKClient } = await import('../sdk.js');
|
|
290
|
+
const sdk = new SDKClient(profile.instance_url, app.auth);
|
|
291
|
+
flag = await select({
|
|
292
|
+
message: `Modify ${name} (${profile.instance_url})`,
|
|
293
|
+
choices,
|
|
294
|
+
});
|
|
295
|
+
if (flag === 'domain') {
|
|
296
|
+
const domainsMod = await import('./dev/domains.js');
|
|
297
|
+
const installed = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
298
|
+
if (!installed) {
|
|
299
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
300
|
+
}
|
|
301
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
302
|
+
const clear = profile.domain
|
|
303
|
+
? await confirm({ message: `Domain is set. Clear it?`, default: false })
|
|
304
|
+
: false;
|
|
305
|
+
if (clear) {
|
|
306
|
+
profile.domain = '';
|
|
307
|
+
await saveConfig(app.config);
|
|
308
|
+
app.ok({ profile: name, domain: '' }, { summary: `Domain cleared for ${name}` });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const sysID = await domainsMod.pickDomain({ ...app, sdk });
|
|
312
|
+
if (sysID) {
|
|
313
|
+
profile.domain = sysID;
|
|
314
|
+
await saveConfig(app.config);
|
|
315
|
+
app.ok({ profile: name, domain: sysID }, { summary: `Domain set for ${name}` });
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
profile[flag] = !profile[flag];
|
|
322
|
+
await saveConfig(app.config);
|
|
323
|
+
app.ok({ profile: name, [flag]: profile[flag] },
|
|
324
|
+
{ summary: `${flag} is now ${profile[flag] ? 'ON' : 'off'} for ${name}` });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Delete an instance entry and its stored credentials (shared by the
|
|
329
|
+
* `auth remove` subcommand and the `jsn setup` hub).
|
|
330
|
+
*/
|
|
331
|
+
export async function removeProfile(app, name) {
|
|
332
|
+
if (!app.config.profiles[name]) {
|
|
333
|
+
throw new Error(`Profile not found: ${name}`);
|
|
334
|
+
}
|
|
335
|
+
const instance = app.config.profiles[name].instance_url;
|
|
336
|
+
delete app.config.profiles[name];
|
|
337
|
+
if (app.config.defaultProfile === name) app.config.defaultProfile = '';
|
|
338
|
+
if (app.config.activeProfile === name) app.config.activeProfile = '';
|
|
339
|
+
saveConfig(app.config);
|
|
340
|
+
|
|
341
|
+
// Only clear credentials if no other profile uses this instance URL
|
|
342
|
+
const stillInUse = instance && Object.values(app.config.profiles || {})
|
|
343
|
+
.some(p => p.instance_url === instance);
|
|
344
|
+
if (instance && !stillInUse) {
|
|
345
|
+
app.auth.logout(instance);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
|
|
349
|
+
}
|
|
350
|
+
|
|
168
351
|
export function authCmd(wrap) {
|
|
169
352
|
return {
|
|
170
353
|
command: 'auth [subcommand]',
|
|
@@ -463,6 +646,23 @@ Examples:
|
|
|
463
646
|
|
|
464
647
|
const creds = await app.auth.getCredentialsFor(instanceURL);
|
|
465
648
|
const refreshed = await app.auth.refreshToken(instanceURL, creds);
|
|
649
|
+
|
|
650
|
+
// Re-probe domain separation on refresh so the capability flag
|
|
651
|
+
// stays in sync if the plugin was installed/removed since setup.
|
|
652
|
+
try {
|
|
653
|
+
const { SDKClient } = await import('../sdk.js');
|
|
654
|
+
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
655
|
+
const sdk = new SDKClient(instanceURL, app.auth);
|
|
656
|
+
const hasDS = await isDomainSeparationInstalled({ sdk });
|
|
657
|
+
const name = app.config.activeProfile || app.config.defaultProfile;
|
|
658
|
+
if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
|
|
659
|
+
app.config.profiles[name].domain_separation = hasDS;
|
|
660
|
+
await saveConfig(app.config);
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
// Non-fatal — capability probe is best-effort
|
|
664
|
+
}
|
|
665
|
+
|
|
466
666
|
app.ok({
|
|
467
667
|
refreshed: true,
|
|
468
668
|
instance: instanceURL,
|
|
@@ -507,6 +707,22 @@ Examples:
|
|
|
507
707
|
app.ok({ active_profile: name }, { summary: `Active profile: ${name}` });
|
|
508
708
|
}),
|
|
509
709
|
})
|
|
710
|
+
.command({
|
|
711
|
+
command: 'modify [name]',
|
|
712
|
+
describe: 'Toggle instance configuration (read-only, skip confirmations, domain)',
|
|
713
|
+
builder: (sub) => sub
|
|
714
|
+
.positional('name', {
|
|
715
|
+
describe: 'Profile name to modify',
|
|
716
|
+
type: 'string',
|
|
717
|
+
})
|
|
718
|
+
.option('domain', {
|
|
719
|
+
type: 'string',
|
|
720
|
+
describe: 'Set the domain for this profile (name, sys_id, or "clear")',
|
|
721
|
+
}),
|
|
722
|
+
handler: wrap(async (argv, app) => {
|
|
723
|
+
await modifyProfile(app, argv);
|
|
724
|
+
}),
|
|
725
|
+
})
|
|
510
726
|
.command({
|
|
511
727
|
command: 'remove [name]',
|
|
512
728
|
describe: 'Delete an instance entry and its stored credentials',
|
|
@@ -518,45 +734,9 @@ Examples:
|
|
|
518
734
|
handler: wrap(async (argv, app) => {
|
|
519
735
|
let name = argv.name;
|
|
520
736
|
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
|
-
}
|
|
737
|
+
name = await pickProfile(app, 'Remove which instance?');
|
|
542
738
|
}
|
|
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}` });
|
|
739
|
+
await removeProfile(app, name);
|
|
560
740
|
}),
|
|
561
741
|
});
|
|
562
742
|
},
|
|
@@ -565,15 +745,11 @@ Examples:
|
|
|
565
745
|
console.log('');
|
|
566
746
|
console.log('Available subcommands:');
|
|
567
747
|
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
748
|
console.log(' refresh Refresh the access token');
|
|
571
|
-
console.log('
|
|
572
|
-
console.log(' remove <name> Delete an instance entry and its credentials');
|
|
749
|
+
console.log(' status Show auth status for all profiles');
|
|
573
750
|
console.log('');
|
|
751
|
+
console.log('Tip: "jsn setup" is the interactive manager — add, switch, remove, or modify instances.');
|
|
574
752
|
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
753
|
},
|
|
578
754
|
};
|
|
579
755
|
}
|