@jacebenson/jsn 4.0.3 → 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/package.json +1 -1
- package/src/app.js +25 -3
- package/src/cli.js +3 -1
- package/src/commands/auth.js +116 -7
- 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 +3 -2
- package/src/context.js +52 -5
- package/src/help.js +16 -2
- package/src/mutations.js +10 -1
- package/src/sdk.js +4 -0
package/package.json
CHANGED
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
|
@@ -151,6 +151,36 @@ export async function loginWizard(app, argv = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
console.log('Login successful!');
|
|
153
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
|
+
}
|
|
154
184
|
}
|
|
155
185
|
} else {
|
|
156
186
|
rl.close();
|
|
@@ -210,24 +240,82 @@ const PROFILE_FLAGS = [
|
|
|
210
240
|
];
|
|
211
241
|
|
|
212
242
|
/**
|
|
213
|
-
*
|
|
214
|
-
* Interactive
|
|
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.
|
|
215
245
|
*/
|
|
216
246
|
export async function modifyProfile(app, argv = {}) {
|
|
217
247
|
const name = argv.name || await pickProfile(app, 'Modify which instance?');
|
|
218
248
|
const profile = app.config.profiles?.[name];
|
|
219
249
|
if (!profile) throw new Error(`Profile not found: ${name}`);
|
|
220
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
|
+
|
|
221
276
|
let flag = argv.flag;
|
|
222
277
|
if (!flag) {
|
|
223
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);
|
|
224
291
|
flag = await select({
|
|
225
292
|
message: `Modify ${name} (${profile.instance_url})`,
|
|
226
|
-
choices
|
|
227
|
-
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
228
|
-
value: f.value,
|
|
229
|
-
})),
|
|
293
|
+
choices,
|
|
230
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
|
+
}
|
|
231
319
|
}
|
|
232
320
|
|
|
233
321
|
profile[flag] = !profile[flag];
|
|
@@ -558,6 +646,23 @@ Examples:
|
|
|
558
646
|
|
|
559
647
|
const creds = await app.auth.getCredentialsFor(instanceURL);
|
|
560
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
|
+
|
|
561
666
|
app.ok({
|
|
562
667
|
refreshed: true,
|
|
563
668
|
instance: instanceURL,
|
|
@@ -604,11 +709,15 @@ Examples:
|
|
|
604
709
|
})
|
|
605
710
|
.command({
|
|
606
711
|
command: 'modify [name]',
|
|
607
|
-
describe: 'Toggle instance configuration (read-only, skip confirmations)',
|
|
712
|
+
describe: 'Toggle instance configuration (read-only, skip confirmations, domain)',
|
|
608
713
|
builder: (sub) => sub
|
|
609
714
|
.positional('name', {
|
|
610
715
|
describe: 'Profile name to modify',
|
|
611
716
|
type: 'string',
|
|
717
|
+
})
|
|
718
|
+
.option('domain', {
|
|
719
|
+
type: 'string',
|
|
720
|
+
describe: 'Set the domain for this profile (name, sys_id, or "clear")',
|
|
612
721
|
}),
|
|
613
722
|
handler: wrap(async (argv, app) => {
|
|
614
723
|
await modifyProfile(app, argv);
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Domain separation commands — list/show/current/set
|
|
2
|
+
//
|
|
3
|
+
// Domain separation (plugin) adds a `domain` table holding the domain
|
|
4
|
+
// hierarchy. Each domain-separated table's rows carry `sys_domain` +
|
|
5
|
+
// `sys_domain_path`, and queries are auto-filtered to the session domain
|
|
6
|
+
// plus its children. There is NO durable platform-side "current domain"
|
|
7
|
+
// preference a CLI can write (the picker sets the in-memory session only,
|
|
8
|
+
// and `sys_user.domain` is write-protected even for admin). So jsn stores
|
|
9
|
+
// the chosen domain in the active profile and sends it on every request
|
|
10
|
+
// via the `X-Now-Domain` header — the same scoping the platform applies
|
|
11
|
+
// to its own picker sessions.
|
|
12
|
+
|
|
13
|
+
import { getStringField, interactiveList, assertSafeExactMatch } from '../../helpers.js';
|
|
14
|
+
|
|
15
|
+
const DETAIL_FIELDS = 'sys_id,name,parent,sys_domain_path,description,active,sys_created_on,sys_updated_on';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gate: domain separation must be installed. The `domain` table only
|
|
19
|
+
* exists on instances with the plugin. A single cheap query detects it;
|
|
20
|
+
* on non-DS instances the API returns "Invalid table domain".
|
|
21
|
+
*/
|
|
22
|
+
export async function isDomainSeparationInstalled(app) {
|
|
23
|
+
try {
|
|
24
|
+
const params = new URLSearchParams();
|
|
25
|
+
params.set('sysparm_limit', '1');
|
|
26
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
27
|
+
await app.sdk.list('domain', params);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function requireDomainSeparation(app) {
|
|
35
|
+
if (!(await isDomainSeparationInstalled(app))) {
|
|
36
|
+
const e = new Error('Domain separation is not installed on this instance — the `domain` table does not exist. Install the Domain Separation plugin to use these commands.');
|
|
37
|
+
e.name = 'NotSupported';
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a domain name or sys_id to its sys_id. Throws when not found.
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveDomainSysId(app, domainArg) {
|
|
46
|
+
assertSafeExactMatch(domainArg);
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
params.set('sysparm_query', `name=${domainArg}`);
|
|
49
|
+
params.set('sysparm_limit', '1');
|
|
50
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
51
|
+
let records = await app.sdk.list('domain', params);
|
|
52
|
+
if (records.length === 0) {
|
|
53
|
+
const p2 = new URLSearchParams();
|
|
54
|
+
p2.set('sysparm_query', `sys_id=${domainArg}`);
|
|
55
|
+
p2.set('sysparm_limit', '1');
|
|
56
|
+
p2.set('sysparm_fields', 'sys_id,name');
|
|
57
|
+
records = await app.sdk.list('domain', p2);
|
|
58
|
+
}
|
|
59
|
+
if (records.length === 0) {
|
|
60
|
+
throw new Error(`Domain not found: ${domainArg}`);
|
|
61
|
+
}
|
|
62
|
+
return getStringField(records[0], 'sys_id');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Interactive domain picker — returns the chosen domain's sys_id, or null
|
|
67
|
+
* when the user cancels. Used by `domains set` and the setup/modify flows.
|
|
68
|
+
*/
|
|
69
|
+
export async function pickDomain(app) {
|
|
70
|
+
const picked = await interactiveList({
|
|
71
|
+
app, table: 'domain', singular: 'domain', columns: ['name', 'sys_domain_path'], labelField: 'name',
|
|
72
|
+
formatLabel: formatDomainLabel,
|
|
73
|
+
});
|
|
74
|
+
if (!picked) return null;
|
|
75
|
+
return getStringField(picked, 'sys_id');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Format a picker label: "Name (path)". */
|
|
79
|
+
export function formatDomainLabel(r) {
|
|
80
|
+
const path = getStringField(r, 'sys_domain_path');
|
|
81
|
+
return `${getStringField(r, 'name')}${path ? ` (${path})` : ''}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Build a rich detail view for a domain record. */
|
|
85
|
+
export async function formatDomainDetail(app, record) {
|
|
86
|
+
const sysID = getStringField(record, 'sys_id');
|
|
87
|
+
const name = getStringField(record, 'name');
|
|
88
|
+
const instance = app.getEffectiveInstance();
|
|
89
|
+
const link = `${instance}/domain.do?sys_id=${sysID}`;
|
|
90
|
+
|
|
91
|
+
// Fetch full record for fields not present on the picked row
|
|
92
|
+
const params = new URLSearchParams();
|
|
93
|
+
params.set('sysparm_query', `sys_id=${sysID}`);
|
|
94
|
+
params.set('sysparm_limit', '1');
|
|
95
|
+
params.set('sysparm_display_value', 'all');
|
|
96
|
+
params.set('sysparm_fields', DETAIL_FIELDS);
|
|
97
|
+
const full = await app.sdk.list('domain', params);
|
|
98
|
+
const rec = full[0] || record;
|
|
99
|
+
|
|
100
|
+
const parent = getStringField(rec, 'parent');
|
|
101
|
+
const path = getStringField(rec, 'sys_domain_path');
|
|
102
|
+
|
|
103
|
+
const lines = [
|
|
104
|
+
`Domain: ${name}`,
|
|
105
|
+
`Path: ${path || '(root)'}`,
|
|
106
|
+
`Parent: ${parent || '(none)'}`,
|
|
107
|
+
`Active: ${getStringField(rec, 'active') || 'true'}`,
|
|
108
|
+
`Created: ${getStringField(rec, 'sys_created_on')}`,
|
|
109
|
+
`Updated: ${getStringField(rec, 'sys_updated_on')}`,
|
|
110
|
+
`Link: ${link}`,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const desc = getStringField(rec, 'description');
|
|
114
|
+
if (desc) lines.splice(1, 0, `Desc: ${desc}`);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
sys_id: sysID,
|
|
118
|
+
name,
|
|
119
|
+
parent,
|
|
120
|
+
sys_domain_path: path,
|
|
121
|
+
_formatted: lines.join('\n'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function domainsCmd(wrap) {
|
|
126
|
+
return {
|
|
127
|
+
command: 'domains [subcommand]',
|
|
128
|
+
aliases: ['domain', 'dom'],
|
|
129
|
+
describe: 'Manage domains (domain separation)',
|
|
130
|
+
builder: (yargs) => {
|
|
131
|
+
return yargs
|
|
132
|
+
.command({
|
|
133
|
+
command: 'list',
|
|
134
|
+
aliases: ['ls'],
|
|
135
|
+
describe: 'List domains (hierarchy + paths)',
|
|
136
|
+
builder: (y) => y
|
|
137
|
+
.option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEACME")' })
|
|
138
|
+
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns' })
|
|
139
|
+
.option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
|
|
140
|
+
handler: wrap(async (argv, app) => {
|
|
141
|
+
await requireDomainSeparation(app);
|
|
142
|
+
const columns = argv.columns ? argv.columns.split(',') : ['name', 'parent', 'sys_domain_path'];
|
|
143
|
+
const query = argv.query || '';
|
|
144
|
+
|
|
145
|
+
const picked = await interactiveList({
|
|
146
|
+
app, table: 'domain', singular: 'domain', columns, limit: argv.limit, query, labelField: 'name',
|
|
147
|
+
formatLabel: formatDomainLabel,
|
|
148
|
+
});
|
|
149
|
+
if (picked === undefined) return; // user cancelled
|
|
150
|
+
if (picked) {
|
|
151
|
+
const detail = await formatDomainDetail(app, picked);
|
|
152
|
+
return app.ok(detail, { summary: `Domain: ${getStringField(picked, 'name')}` });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const params = new URLSearchParams();
|
|
156
|
+
params.set('sysparm_limit', String(argv.limit));
|
|
157
|
+
params.set('sysparm_display_value', 'all');
|
|
158
|
+
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
159
|
+
const q = argv.query ? argv.query + '^ORDERBYsys_domain_path' : 'ORDERBYsys_domain_path';
|
|
160
|
+
params.set('sysparm_query', q);
|
|
161
|
+
const records = await app.sdk.list('domain', params);
|
|
162
|
+
app.ok({
|
|
163
|
+
table: 'domain',
|
|
164
|
+
count: records.length,
|
|
165
|
+
columns,
|
|
166
|
+
records: records.map(r => formatDomainListRecord(r, columns)),
|
|
167
|
+
context: { instance_url: app.getEffectiveInstance() },
|
|
168
|
+
}, { summary: `${records.length} domain(s)` });
|
|
169
|
+
}),
|
|
170
|
+
})
|
|
171
|
+
.command({
|
|
172
|
+
command: 'show <domain>',
|
|
173
|
+
aliases: ['get'],
|
|
174
|
+
describe: 'Show a domain by name or sys_id',
|
|
175
|
+
builder: (y) => y
|
|
176
|
+
.positional('domain', {
|
|
177
|
+
describe: 'Domain name (e.g. ACME) or sys_id',
|
|
178
|
+
type: 'string',
|
|
179
|
+
}),
|
|
180
|
+
handler: wrap(async (argv, app) => {
|
|
181
|
+
await requireDomainSeparation(app);
|
|
182
|
+
assertSafeExactMatch(argv.domain);
|
|
183
|
+
let records;
|
|
184
|
+
// Try by name first, then by sys_id
|
|
185
|
+
for (const queryField of ['name', 'sys_id']) {
|
|
186
|
+
const params = new URLSearchParams();
|
|
187
|
+
params.set('sysparm_query', `${queryField}=${argv.domain}`);
|
|
188
|
+
params.set('sysparm_limit', '1');
|
|
189
|
+
params.set('sysparm_display_value', 'all');
|
|
190
|
+
records = await app.sdk.list('domain', params);
|
|
191
|
+
if (records.length > 0) break;
|
|
192
|
+
}
|
|
193
|
+
if (!records || records.length === 0) {
|
|
194
|
+
throw new Error(`Domain not found: ${argv.domain}`);
|
|
195
|
+
}
|
|
196
|
+
const detail = await formatDomainDetail(app, records[0]);
|
|
197
|
+
app.ok(detail, { summary: `Domain ${argv.domain}` });
|
|
198
|
+
}),
|
|
199
|
+
})
|
|
200
|
+
.command({
|
|
201
|
+
command: 'current',
|
|
202
|
+
describe: 'Show the configured domain for this profile',
|
|
203
|
+
handler: wrap(async (argv, app) => {
|
|
204
|
+
await requireDomainSeparation(app);
|
|
205
|
+
const domain = app._profileDomain(app.config);
|
|
206
|
+
if (!domain) {
|
|
207
|
+
app.ok({ domain: '', scoping: 'none' }, {
|
|
208
|
+
summary: 'No domain configured — requests are not domain-scoped',
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Resolve the configured value to a display name if it's a sys_id
|
|
213
|
+
let label = domain;
|
|
214
|
+
try {
|
|
215
|
+
const params = new URLSearchParams();
|
|
216
|
+
params.set('sysparm_query', `sys_id=${domain}`);
|
|
217
|
+
params.set('sysparm_limit', '1');
|
|
218
|
+
params.set('sysparm_fields', 'sys_id,name');
|
|
219
|
+
const records = await app.sdk.list('domain', params);
|
|
220
|
+
if (records.length > 0) label = `${getStringField(records[0], 'name')} (${domain})`;
|
|
221
|
+
} catch { /* non-fatal — show raw value */ }
|
|
222
|
+
app.ok({ domain, scoping: 'X-Now-Domain header' }, {
|
|
223
|
+
summary: `Configured domain: ${label}`,
|
|
224
|
+
});
|
|
225
|
+
}),
|
|
226
|
+
})
|
|
227
|
+
.command({
|
|
228
|
+
command: 'set [domain]',
|
|
229
|
+
describe: 'Set the configured domain for this profile (interactive picker when run bare; omit to clear)',
|
|
230
|
+
handler: wrap(async (argv, app) => {
|
|
231
|
+
await requireDomainSeparation(app);
|
|
232
|
+
let domainArg = argv.domain;
|
|
233
|
+
if (!domainArg) {
|
|
234
|
+
const picked = await pickDomain(app);
|
|
235
|
+
if (!picked) return; // cancelled or non-interactive
|
|
236
|
+
domainArg = picked;
|
|
237
|
+
} else if (domainArg.toLowerCase() === 'none' || domainArg.toLowerCase() === 'clear') {
|
|
238
|
+
app.setDomain('');
|
|
239
|
+
app.ok({ domain: '' }, { summary: 'Domain cleared — requests are no longer domain-scoped' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sysID = await resolveDomainSysId(app, domainArg);
|
|
244
|
+
app.setDomain(sysID);
|
|
245
|
+
app.ok({ domain: sysID, name: domainArg }, {
|
|
246
|
+
summary: `Configured domain: ${domainArg} — all requests scoped to it`,
|
|
247
|
+
});
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
handler: async (argv) => {
|
|
252
|
+
if (!argv._[1]) {
|
|
253
|
+
const app = argv.app;
|
|
254
|
+
let installed = false;
|
|
255
|
+
try {
|
|
256
|
+
if (app && app.sdk) installed = await isDomainSeparationInstalled(app);
|
|
257
|
+
} catch {
|
|
258
|
+
installed = false;
|
|
259
|
+
}
|
|
260
|
+
if (!installed) {
|
|
261
|
+
if (app && app.sdk) {
|
|
262
|
+
console.log('Domain separation is not installed on this instance.');
|
|
263
|
+
console.log('Install the Domain Separation plugin to manage domains.');
|
|
264
|
+
} else {
|
|
265
|
+
console.log('No instance configured — run "jsn auth login <instance>" first.');
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log('Manage ServiceNow domains (domain separation).\n');
|
|
270
|
+
console.log('Commands:');
|
|
271
|
+
console.log(' list List domains (hierarchy + paths)');
|
|
272
|
+
console.log(' show <domain> Show a domain');
|
|
273
|
+
console.log(' current Show the configured domain for this profile');
|
|
274
|
+
console.log(' set [domain] Configure the domain for this profile (picker when bare; "clear" to unset)');
|
|
275
|
+
console.log('\nDomains are applied per-request via the X-Now-Domain header —');
|
|
276
|
+
console.log('the platform filters every query to the domain and its children.');
|
|
277
|
+
console.log('Run "jsn domains <command> --help" for details.');
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Flat display record for list mode (keeps the JSON payload small). */
|
|
284
|
+
function formatDomainListRecord(r, columns) {
|
|
285
|
+
const out = {};
|
|
286
|
+
for (const c of columns) {
|
|
287
|
+
const v = getStringField(r, c);
|
|
288
|
+
out[c] = v;
|
|
289
|
+
}
|
|
290
|
+
out.sys_id = getStringField(r, 'sys_id');
|
|
291
|
+
return out;
|
|
292
|
+
}
|