@jacebenson/jsn 4.3.13-dev.93.1 → 4.3.13-dev.94.1

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 CHANGED
@@ -78,3 +78,17 @@ npm test
78
78
  ## License
79
79
 
80
80
  MIT
81
+
82
+
83
+ ### Commands for the connected account
84
+
85
+ Login and OAuth refresh check for the current account's `admin` role. Profiles
86
+ without that role show a compact command list focused on tickets, generic
87
+ records, people/groups, documentation, and connection management. If the role
88
+ check is unavailable, the profile also uses compact help; this does not assert
89
+ that the account lacks admin. Existing profiles keep full help until renewed.
90
+
91
+ This applies to root help, root JSON, `discover`, and root shell completion.
92
+ Use `jsn --all-commands --help` or `jsn discover --all-commands --json` to inspect
93
+ every registered command. Explicit command paths remain available and ServiceNow
94
+ ACLs decide access. Reauthenticate (or refresh OAuth) after roles change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.13-dev.93.1",
3
+ "version": "4.3.13-dev.94.1",
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": {
@@ -332,6 +332,10 @@ export class LocalCLI {
332
332
  }
333
333
 
334
334
  _help(node, path = []) {
335
+ if (!path.length && this.rootHelpRenderer) {
336
+ process.stdout.write(this.rootHelpRenderer() + "\n");
337
+ return;
338
+ }
335
339
  const lines = [node.usageText || `Usage: ${this.script} ${path.join(' ') || '<command>'}`];
336
340
  if (node.definition.describe) lines.push(`\n${node.definition.describe}`);
337
341
  if (node.children.length) lines.push('\nCommands:\n' + node.children.filter((n) => n.name !== this.completionCommand).map((n) => ` ${n.definition.command} ${n.definition.describe || ''}`).join('\n'));
package/src/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { visibleCommandRegistry } from './command-audience.js';
1
2
  // Root CLI using the local yargs-compatible adapter
2
3
 
3
4
  import yargs, { hideBin } from './cli-adapter.js';
@@ -148,6 +149,7 @@ export function buildCLI() {
148
149
  },
149
150
  ])
150
151
  .options({
152
+ 'all-commands': { type: 'boolean', describe: 'Show the full command list, including admin and developer tools' },
151
153
  reason: { type: 'string', describe: 'Optional non-sensitive reason saved in local mutation history' },
152
154
 
153
155
  force: {
@@ -432,7 +434,7 @@ export function buildCLI() {
432
434
  .command(skillCmd(wrap))
433
435
  .command(docsCmd(wrap))
434
436
  .command(versionCmd(wrap))
435
- .command(discoverCmd(wrap, () => cliInstance.commandRegistry()))
437
+ .command(discoverCmd(wrap, (argv) => visibleCommandRegistry(cliInstance.commandRegistry(), app.config, argv)))
436
438
  .demandCommand(1, 'You must specify a command')
437
439
  // Completion plumbing under a hidden command name, so the explicit
438
440
  // 'completion' command below owns the UX. (#176)
@@ -452,6 +454,12 @@ export function buildCLI() {
452
454
  if (current && !current.startsWith('-') && depth === 0) {
453
455
  out = [...new Set([...out, ...rootCommands])];
454
456
  }
457
+ const options = cliInstance.parseGlobalOptions(process.argv.slice(2));
458
+ const visible = visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(options), options);
459
+ if (!depth) {
460
+ const names = new Set(visible.commands.flatMap(command => [command.name, ...command.aliases]));
461
+ out = out.filter(value => value.startsWith('-') || names.has(value));
462
+ }
455
463
  done([...new Set(out)]);
456
464
  });
457
465
  })
@@ -557,7 +565,7 @@ export function buildCLI() {
557
565
  if (typeof formatArgv?.get === 'string' && formatArgv.get) {
558
566
  app.output.setJqFilter(formatArgv.get);
559
567
  }
560
- const { commands } = cliInstance.commandRegistry();
568
+ const { commands } = visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(formatArgv), formatArgv);
561
569
  try {
562
570
  app.ok({ commands }, {
563
571
  summary: `${commands.length} registered commands — run "jsn <command> --json" for a command's subcommands.`,
@@ -567,7 +575,7 @@ export function buildCLI() {
567
575
  }
568
576
  process.exit(0);
569
577
  }
570
- process.stdout.write(renderHelp(loadConfig(), cliInstance.commandRegistry()) + '\n');
578
+ process.stdout.write(renderHelp(loadConfig(formatArgv), visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(formatArgv), formatArgv)) + '\n');
571
579
  process.exit(0);
572
580
  }
573
581
  // Every other failure here (unknown command, missing required argument,
@@ -587,6 +595,12 @@ export function buildCLI() {
587
595
  exitWithError(new AppError(CodeUsage, msg, hint), { app });
588
596
  });
589
597
 
598
+ cliInstance.rootHelpRenderer = () => {
599
+ const options = cliInstance.parseGlobalOptions(process.argv.slice(2));
600
+ const config = loadConfig(options);
601
+ return renderHelp(config, visibleCommandRegistry(cliInstance.commandRegistry(), config, options));
602
+ };
603
+
590
604
  // Snapshot the root command names (plus aliases) for the completion
591
605
  // filter. Done before any parse — command builders mutate the registry.
592
606
  rootCommands = cliInstance.getInternalMethods().getCommandInstance().getCommands();
@@ -0,0 +1,35 @@
1
+ // Presentation policy only. ServiceNow ACLs still authorize every request.
2
+ // Explicit command paths and --all-commands remain available to every account.
3
+ const BASIC_COMMANDS = new Set([
4
+ 'incidents', 'changes', 'requests', 'tasks', 'tickets', 'approvals', 'records',
5
+ 'groups', 'groupmembers', 'grouproles', 'users', 'setup', 'auth', 'skill',
6
+ 'version', 'docs', 'history', 'discover', 'completion',
7
+ ]);
8
+
9
+ export async function detectCommandAudience(sdk, username, instanceURL = sdk?.baseURL) {
10
+ if (!username || typeof sdk?.listStrict !== 'function') return undefined;
11
+ try {
12
+ const roles = await sdk.listStrict('sys_user_has_role', {
13
+ sysparm_query: 'user=javascript:gs.getUserID()^role.name=admin',
14
+ sysparm_fields: 'sys_id', sysparm_limit: '1',
15
+ }, { touchLastSeen: false });
16
+ return { username, instance_url: instanceURL, mode: roles.length ? 'admin' : 'basic', role_check: 'verified' };
17
+ } catch {
18
+ // Missing role access must not expand help. This is a presentation fallback,
19
+ // not a claim that the role query succeeded or an authorization boundary.
20
+ return { username, instance_url: instanceURL, mode: 'basic', role_check: 'unavailable' };
21
+ }
22
+ }
23
+
24
+ export function commandAudience(config, { instance, allCommands = false } = {}) {
25
+ if (allCommands || instance) return 'all';
26
+ const name = config?.activeProfile || config?.defaultProfile;
27
+ const profile = config?.profiles?.[name];
28
+ const cached = profile?.command_audience;
29
+ return cached?.username === profile?.username && cached?.instance_url === profile?.instance_url && cached?.mode === 'basic' ? 'basic' : 'all';
30
+ }
31
+
32
+ export function visibleCommandRegistry(registry, config, options = {}) {
33
+ if (commandAudience(config, options) !== 'basic') return registry;
34
+ return { ...registry, commands: registry.commands.filter(command => BASIC_COMMANDS.has(command.name)) };
35
+ }
@@ -1,3 +1,4 @@
1
+ import { detectCommandAudience } from '../command-audience.js';
1
2
  import { getEffectiveInstance, normalizeInstanceURL, saveConfig, setActiveProfile } from '../config.js';
2
3
  import { errUsage, errUsageHint, errConfirmationRequired } from '../errors.js';
3
4
  import { canUsePicker, formatCommandList, quoteShellArg } from '../helpers.js';
@@ -710,6 +711,8 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
710
711
  // profile exists; preserve it when verification is unavailable.
711
712
  username = username || targetUsername;
712
713
 
714
+ const commandAudience = await detectCommandAudience(app.sdk, username, instanceURL);
715
+
713
716
  // Save to profiles — deduplicate by instance URL
714
717
  if (!app.config.profiles) {
715
718
  app.config.profiles = {};
@@ -740,7 +743,9 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
740
743
  read_only: preferences.read_only,
741
744
  skip_confirmations: preferences.skip_confirmations,
742
745
  include_counts: preferences.include_counts,
746
+ ...(commandAudience ? { command_audience: commandAudience } : {}),
743
747
  };
748
+ if (!commandAudience) delete app.config.profiles[profileName].command_audience;
744
749
  if (!targetProfile && interactive) {
745
750
  try {
746
751
  const domains = await import('./domains.js');
@@ -1050,12 +1055,22 @@ Examples:
1050
1055
  const name = target.name || app.config.activeProfile || app.config.defaultProfile;
1051
1056
  if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
1052
1057
  app.config.profiles[name].domain_separation = hasDS;
1058
+
1053
1059
  await saveConfig(app.config);
1054
1060
  }
1055
1061
  } catch {
1056
1062
  // Non-fatal — capability probe is best-effort
1057
1063
  }
1058
1064
 
1065
+ const audienceName = target.name || app.config.activeProfile || app.config.defaultProfile;
1066
+ const audienceProfile = app.config.profiles?.[audienceName];
1067
+ if (audienceProfile?.instance_url === instanceURL) {
1068
+ const sdk = app.getSDKForProfile
1069
+ ? app.getSDKForProfile(instanceURL, { ...targetAuthOptions, profile: audienceProfile }) : app.sdk;
1070
+ audienceProfile.command_audience = await detectCommandAudience(sdk, audienceProfile.username, instanceURL);
1071
+ await saveConfig(app.config);
1072
+ }
1073
+
1059
1074
  app.ok({
1060
1075
  refreshed: true,
1061
1076
  instance: instanceURL,
@@ -35,7 +35,7 @@ export function discoverCmd(wrap, getRegistry) {
35
35
  'Use --json for structured results or --json --get data.commands for only matches.',
36
36
  ].join('\n')),
37
37
  handler: wrap(async (argv, app) => {
38
- const commands = discoverCommands(getRegistry(), { query: argv.query, table: argv.table });
38
+ const commands = discoverCommands(getRegistry(argv), { query: argv.query, table: argv.table });
39
39
  const lines = commands.flatMap(command => [
40
40
  `${command.name}: ${command.description}`,
41
41
  ...(command.discovery ? [
package/src/help.js CHANGED
@@ -203,6 +203,8 @@ function renderFlags(options) {
203
203
  export function renderTips() {
204
204
  return `
205
205
  TIPS
206
+ Show every command, including admin and developer tools: "jsn --all-commands --help"
207
+ Command visibility follows the selected profile at authentication; explicit commands still use ServiceNow ACLs.
206
208
  Find commands by task words: "jsn discover catalog"; by configured table: "jsn discover --table incident"
207
209
  Cross-table task searches and custom tables: "jsn records list --help"
208
210
  Specialized commands offer configured columns, lookup, and table-specific views; inspect discovery results for details