@jacebenson/jsn 3.1.4 → 3.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
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": {
package/src/auth.js CHANGED
@@ -7,8 +7,8 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import crypto from 'node:crypto';
10
- import readline from 'node:readline';
11
10
  import { execSync } from 'node:child_process';
11
+ import { password as passwordPrompt } from '@inquirer/prompts';
12
12
  import { globalConfigDir, normalizeInstanceURL } from './config.js';
13
13
  import { errAuth } from './errors.js';
14
14
 
@@ -248,52 +248,7 @@ function deleteCredentials(instance, username) {
248
248
  }
249
249
 
250
250
  function askHidden(promptText) {
251
- return new Promise((resolve) => {
252
- const rl = readline.createInterface({
253
- input: process.stdin,
254
- output: process.stdout,
255
- });
256
-
257
- const stdin = process.stdin;
258
- const stdout = process.stdout;
259
-
260
- if (!stdin.isTTY) {
261
- rl.question(promptText, (answer) => {
262
- rl.close();
263
- resolve(answer.trim());
264
- });
265
- return;
266
- }
267
-
268
- stdout.write(promptText);
269
-
270
- stdin.setRawMode(true);
271
- stdin.resume();
272
- stdin.setEncoding('utf-8');
273
-
274
- let input = '';
275
- const onData = (key) => {
276
- if (key === '\r' || key === '\n') {
277
- stdin.removeListener('data', onData);
278
- stdin.setRawMode(false);
279
- stdin.pause();
280
- stdout.write('\n');
281
- rl.close();
282
- resolve(input);
283
- } else if (key === '\u0003') {
284
- process.exit();
285
- } else if (key === '\u007f') {
286
- if (input.length > 0) {
287
- input = input.slice(0, -1);
288
- stdout.write('\b \b');
289
- }
290
- } else {
291
- input += key;
292
- stdout.write('*');
293
- }
294
- };
295
- stdin.on('data', onData);
296
- });
251
+ return passwordPrompt({ message: promptText });
297
252
  }
298
253
 
299
254
  export class AuthManager {
package/src/cli.js CHANGED
@@ -35,8 +35,9 @@ import {
35
35
  spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
36
36
  aclsCmd, rolesCmd, propertiesCmd,
37
37
  relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
38
- privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
38
+ privilegesCmd, uxscriptsCmd, aliasesCmd,
39
39
  catalogscriptsCmd, cataloguipoliciesCmd,
40
+ uipoliciesCmd,
40
41
  scriptactionsCmd, scheduledjobsCmd, asyncrulesCmd, triggersCmd,
41
42
  decisiontablesCmd, assignmentsCmd,
42
43
  emailCmd,
@@ -235,7 +236,6 @@ export function buildCLI() {
235
236
  .command(b4rulesCmd(wrap))
236
237
  .command(rolesCmd(wrap))
237
238
  .command(privilegesCmd(wrap))
238
- .command(securitytypesCmd(wrap))
239
239
  .command(aliasesCmd(wrap))
240
240
  .command(groupsCmd(wrap))
241
241
  .command(groupMembersCmd(wrap))
@@ -249,6 +249,7 @@ export function buildCLI() {
249
249
  .command(uiActionsCmd(wrap))
250
250
  .command(viewsCmd(wrap))
251
251
  .command(catalogscriptsCmd(wrap))
252
+ .command(uipoliciesCmd(wrap))
252
253
  .command(cataloguipoliciesCmd(wrap))
253
254
  // USER EXPERIENCE — Core UI
254
255
  .command(uiPagesCmd(wrap))
@@ -73,10 +73,10 @@ export const tablesCmd = (wrap) => buildDevCmd('tables', 'sys_db_object', ['tabl
73
73
  },
74
74
  });
75
75
 
76
- export const columnsCmd = (wrap) => buildDevCmd('columns', 'sys_dictionary', ['column', 'col'], ['element', 'column_label', 'internal_type', 'mandatory', 'max_length', 'active'], wrap, { singular: 'column', scopeValidation: true });
76
+ export { columnsCmd } from './columns.js';
77
77
 
78
78
  // Read-only commands (Go only has list/show)
79
- export const importCmd = (wrap) => buildDevCmd('import', 'sys_import_set', ['imports', 'imp'], ['sys_import_set', 'sys_import_row', 'sys_target_table', 'sys_target_sys_id'], wrap, { singular: 'import set', readOnly: true });
79
+ export { importCmd } from './import.js';
80
80
  export const spPagesCmd = (wrap) => buildDevCmd('sppages', 'sp_page', ['sp-pages', 'pages'], ['id', 'title', 'sys_scope'], wrap, { singular: 'Service Portal page', readOnly: true });
81
81
  export const spWidgetsCmd = (wrap) => buildDevCmd('spwidgets', 'sp_widget', ['sp-widget', 'widgets'], ['id', 'name', 'sys_scope'], wrap, { singular: 'Service Portal widget', readOnly: true });
82
82
  export const uiPagesCmd = (wrap) => buildDevCmd('uipages', 'sys_ui_page', ['ui-page', 'pages'], ['name', 'sys_scope'], wrap, { singular: 'UI page', scopeValidation: true });
@@ -92,10 +92,9 @@ export const relationshipsCmd = (wrap) => buildDevCmd('relationships', 'sys_rela
92
92
  export const appmodulesCmd = (wrap) => buildDevCmd('appmodules', 'sys_app_module', ['appmodule', 'am'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application module', scopeValidation: true });
93
93
  export const listcontrolsCmd = (wrap) => buildDevCmd('listcontrols', 'sys_ui_list_control', ['listcontrol', 'lc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'list control', scopeValidation: true });
94
94
  export const viewsCmd = (wrap) => buildDevCmd('views', 'sys_ui_view', ['view', 'vw'], ['name', 'title', 'sys_scope'], wrap, { singular: 'view', scopeValidation: true });
95
- export const privilegesCmd = (wrap) => buildDevCmd('privileges', 'sys_scope_privilege', ['privilege', 'priv'], ['name', 'status', 'sys_scope'], wrap, { singular: 'privilege', scopeValidation: true });
96
- export const securitytypesCmd = (wrap) => buildDevCmd('securitytypes', 'sys_security_type', ['securitytype', 'st'], ['name', 'active', 'sys_scope'], wrap, { singular: 'security type', scopeValidation: true });
95
+ export const privilegesCmd = (wrap) => buildDevCmd('privileges', 'sys_scope_privilege', ['privilege', 'priv'], ['source_scope', 'target_scope', 'target_name', 'operation', 'status'], wrap, { singular: 'privilege', scopeValidation: true });
97
96
  export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
98
- export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'], ['name', 'table', 'sys_scope'], wrap, { singular: 'alias', scopeValidation: true });
97
+ export { aliasesCmd } from './aliases.js';
99
98
  export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
100
99
 
101
100
  // ── Automation: Async ──
@@ -122,4 +121,5 @@ export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['u
122
121
  // ── UX: Workspaces ──
123
122
  export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
124
123
  export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
125
- export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
124
+ export { uipoliciesCmd } from './uipolicies.js';
125
+ export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'catalog_item', 'variable_set', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
@@ -0,0 +1,30 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { getStringField } from '../../helpers.js';
3
+
4
+ export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'],
5
+ ['name', 'table', 'sys_scope'], wrap, {
6
+ singular: 'alias',
7
+ scopeValidation: true,
8
+ onShow: async (record, app) => {
9
+ const sysID = getStringField(record, 'sys_id') || '';
10
+ if (!sysID) return;
11
+
12
+ const mp = new URLSearchParams();
13
+ mp.set('sysparm_query', `connection_alias=${sysID}^ORDERBYname`);
14
+ mp.set('sysparm_limit', '100');
15
+ mp.set('sysparm_display_value', 'all');
16
+ mp.set('sysparm_fields', 'name,active,credential,sys_id');
17
+ const conns = await app.sdk.list('sys_connection', mp);
18
+
19
+ if (conns.length > 0) {
20
+ console.log(` Connections (${conns.length}):`);
21
+ for (const c of conns) {
22
+ const name = getStringField(c, 'name') || '';
23
+ const cred = getStringField(c, 'credential') || '';
24
+ const cid = getStringField(c, 'sys_id') || '';
25
+ console.log(` ${name}${cred ? ` → ${cred}` : ''}`);
26
+ console.log(` → jsn records get --table sys_connection --sys-id ${cid}`);
27
+ }
28
+ }
29
+ },
30
+ });
@@ -2,4 +2,4 @@ import { buildDevCmd } from './_generic.js';
2
2
 
3
3
  // Before-query business rules — applies filters to GlideRecord queries before they execute.
4
4
  // These are sys_script records with order < 100.
5
- export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'order<100' });
5
+ export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'when=before' });
@@ -0,0 +1,27 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+
3
+ export const columnsCmd = (wrap) => {
4
+ const base = buildDevCmd('columns', 'sys_dictionary', ['column', 'col'],
5
+ ['element', 'column_label', 'internal_type', 'mandatory', 'max_length', 'active'],
6
+ wrap, { singular: 'column', scopeValidation: true });
7
+
8
+ // Add --table filter
9
+ const origBuilder = base.builder;
10
+ base.builder = (yargs) => {
11
+ const built = origBuilder(yargs);
12
+ // Patch the 'list' subcommand builder to add --table
13
+ const listCmd = built._commands?.find(c => c.name === 'list' || c._aliases?.includes('ls'));
14
+ if (listCmd) {
15
+ const origListBuilder = listCmd.builder;
16
+ listCmd.builder = (y) => (origListBuilder ? origListBuilder(y).option('table', { type: 'string', describe: 'Filter by table (e.g. incident)' }) : y.option('table', { type: 'string' }));
17
+ const origHandler = listCmd.handler;
18
+ listCmd.handler = async (argv) => {
19
+ if (argv.table) argv.query = argv.query ? `${argv.query}^name=${argv.table}` : `name=${argv.table}`;
20
+ return origHandler(argv);
21
+ };
22
+ }
23
+ return built;
24
+ };
25
+
26
+ return base;
27
+ };
@@ -0,0 +1,27 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { getStringField } from '../../helpers.js';
3
+
4
+ export const importCmd = (wrap) => buildDevCmd('import', 'sys_import_set', ['imports', 'imp'],
5
+ ['name', 'sys_target_table', 'state', 'sys_created_on'], wrap, {
6
+ singular: 'import set',
7
+ readOnly: true,
8
+ formatLabel: (r) => {
9
+ const name = getStringField(r, 'name') || '';
10
+ const table = getStringField(r, 'sys_target_table') || '';
11
+ return table ? `${name} [${table}]` : name;
12
+ },
13
+ onShow: async (record, app) => {
14
+ const sysID = getStringField(record, 'sys_id') || '';
15
+ if (!sysID) return;
16
+ try {
17
+ const rp = new URLSearchParams();
18
+ rp.set('sysparm_query', `sys_import_set=${sysID}`);
19
+ rp.set('sysparm_limit', '0'); rp.set('sysparm_fields', 'sys_id');
20
+ const rows = await app.sdk.list('sys_import_set_row', rp);
21
+ console.log(` Import rows: ${rows.length}`);
22
+ console.log(` → jsn records list --table sys_import_set_row --query "sys_import_set=${sysID}" --limit 200`);
23
+ } catch (err) {
24
+ console.log(` Import rows: (unavailable)`);
25
+ }
26
+ },
27
+ });
@@ -1,99 +1,68 @@
1
- import chalk from 'chalk';
2
- import { getStringField } from '../../helpers.js';
3
-
4
- // ─── Helpers ───
5
-
6
- function getDisplayValue(record, key) {
7
- if (!record || typeof record !== 'object') return '';
8
- const val = record[key];
9
- if (val == null) return '';
10
- if (typeof val === 'string') return val;
11
- if (typeof val === 'object') {
12
- if (val.display_value != null && val.display_value !== '') return String(val.display_value);
13
- if (val.value != null) return String(val.value);
14
- }
15
- return String(val);
16
- }
17
-
18
- function getIntValue(record, key) {
19
- if (!record || typeof record !== 'object') return 0;
20
- const val = record[key];
21
- if (val == null) return 0;
22
- if (typeof val === 'number') return Math.floor(val);
23
- if (typeof val === 'string') {
24
- const n = parseInt(val, 10);
25
- return isNaN(n) ? 0 : n;
26
- }
27
- if (typeof val === 'object' && val.value != null) {
28
- const n = parseInt(val.value, 10);
29
- return isNaN(n) ? 0 : n;
30
- }
31
- return 0;
32
- }
33
-
34
- // ─── Commands ───
1
+ import { getStringField, interactiveList } from '../../helpers.js';
35
2
 
36
3
  export function listsCmd(wrap) {
37
4
  return {
38
5
  command: 'lists [subcommand]',
39
6
  aliases: ['list-layout', 'l'],
40
- describe: 'Manage UI List layouts',
7
+ describe: 'List views for a table',
41
8
  builder: (yargs) => {
42
9
  return yargs
43
10
  .command({
44
- command: 'list <table>',
11
+ command: 'list [table]',
45
12
  aliases: ['ls'],
46
- describe: 'List list views for a table',
13
+ describe: 'List views for a table (or pick from all tables)',
47
14
  builder: (y) => y
48
- .positional('table', {
49
- describe: 'Table name (e.g. incident, change_request)',
50
- type: 'string',
51
- })
52
- .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
15
+ .positional('table', { type: 'string', describe: 'Table name (e.g. incident)' })
16
+ .option('limit', { alias: 'l', type: 'number', default: 50 }),
53
17
  handler: wrap(async (argv, app) => {
54
18
  app.requireInstance();
55
- const table = argv.table;
56
- const limit = argv.limit || 50;
57
-
58
- const params = new URLSearchParams();
59
- params.set('sysparm_limit', String(limit));
60
- params.set('sysparm_fields', 'view');
61
- params.set('sysparm_group_by', 'view');
62
- params.set('sysparm_display_value', 'all');
63
- const sysparmQuery = table ? `name=${table}^ORDERBYview` : 'ORDERBYview';
64
- params.set('sysparm_query', sysparmQuery);
65
-
66
- const records = await app.sdk.list('sys_ui_list', params);
67
-
68
- const viewMap = new Map();
69
- for (const record of records) {
70
- const view = getDisplayValue(record, 'view');
71
- if (view && !viewMap.has(view)) {
72
- viewMap.set(view, true);
73
- }
19
+ let table = argv.table;
20
+ let viewSysID = null;
21
+
22
+ // If no table, pick one interactively
23
+ if (!table) {
24
+ const tpicked = await interactiveList({
25
+ app, table: 'sys_ui_list', singular: 'table',
26
+ columns: ['name'],
27
+ limit: argv.limit,
28
+ labelField: 'name',
29
+ groupBy: 'name',
30
+ });
31
+ if (!tpicked) return;
32
+ table = getStringField(tpicked, 'name');
74
33
  }
75
34
 
76
- const views = Array.from(viewMap.keys()).sort();
77
- const defaultViews = views.filter(v => v === 'Default view');
78
- const workspaceViews = views.filter(v => v.toLowerCase().includes('workspace'));
79
- const otherViews = views.filter(v => !defaultViews.includes(v) && !workspaceViews.includes(v));
35
+ // Show list views for table
36
+ const picked = await interactiveList({
37
+ app, table: 'sys_ui_list', singular: 'list',
38
+ columns: ['name', 'view'],
39
+ limit: argv.limit,
40
+ query: table ? `name=${table}` : '',
41
+ labelField: 'view',
42
+ formatLabel: (r) => `${getStringField(r, 'view')}`,
43
+ });
80
44
 
81
- const recordsOut = views.map(v => ({ view: v, table }));
45
+ if (picked) {
46
+ const sysID = getStringField(picked, 'sys_id') || '';
47
+ viewSysID = getStringField(picked, 'view') || '';
48
+ await showList(app, table, viewSysID, sysID);
49
+ return;
50
+ }
82
51
 
52
+ // Fallback non-TTY
53
+ const p = new URLSearchParams();
54
+ p.set('sysparm_limit', String(argv.limit));
55
+ p.set('sysparm_display_value', 'all');
56
+ p.set('sysparm_fields', 'name,view');
57
+ p.set('sysparm_query', table ? `name=${table}^ORDERBYview` : 'ORDERBYview');
58
+ const records = await app.sdk.list('sys_ui_list', p);
83
59
  app.ok({
84
- table,
85
- count: views.length,
86
- default: defaultViews,
87
- workspaces: workspaceViews,
88
- other: otherViews,
89
- records: recordsOut,
90
- instance_url: app.getEffectiveInstance(),
91
- }, {
92
- summary: `${views.length} list views for ${table}`,
93
- breadcrumbs: [
94
- { action: 'show', cmd: `jsn lists show ${table} --view "Default view"`, description: 'Show Default view columns' },
95
- ],
96
- });
60
+ table: 'sys_ui_list',
61
+ count: records.length,
62
+ columns: ['name', 'view'],
63
+ records: records.map(r => ({ name: getStringField(r, 'name'), view: getStringField(r, 'view') })),
64
+ context: { instance_url: app.getEffectiveInstance() },
65
+ }, { summary: `${records.length} list view(s)${table ? ` for ${table}` : ''}` });
97
66
  }),
98
67
  })
99
68
  .command({
@@ -101,147 +70,61 @@ export function listsCmd(wrap) {
101
70
  aliases: ['get'],
102
71
  describe: 'Show list layout',
103
72
  builder: (y) => y
104
- .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
73
+ .option('view', { type: 'string', default: 'Default view' }),
105
74
  handler: wrap(async (argv, app) => {
106
75
  app.requireInstance();
107
76
  const table = argv.table;
108
- const viewName = argv.view;
109
77
 
110
- // Look up view sys_id
78
+ // Resolve view sys_id
111
79
  let viewSysID = '';
112
- const viewParams = new URLSearchParams();
113
- viewParams.set('sysparm_limit', '1');
114
- viewParams.set('sysparm_fields', 'sys_id');
115
- viewParams.set('sysparm_query', `name=${viewName}`);
116
- try {
117
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
118
- if (viewRecords.length > 0) {
119
- viewSysID = getStringField(viewRecords[0], 'sys_id');
120
- }
121
- } catch {
122
- // ignore
123
- }
124
-
125
- if (!viewSysID) {
126
- viewParams.set('sysparm_query', `title=${viewName}`);
127
- try {
128
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
129
- if (viewRecords.length > 0) {
130
- viewSysID = getStringField(viewRecords[0], 'sys_id');
131
- }
132
- } catch {
133
- // ignore
134
- }
135
- }
136
-
137
- if (!viewSysID) {
138
- viewSysID = viewName;
139
- }
140
-
141
- // Fetch list layouts
142
- const params = new URLSearchParams();
143
- params.set('sysparm_limit', '10');
144
- params.set('sysparm_fields', 'sys_id,name,view,parent,active,sys_created_on,sys_updated_on');
145
- params.set('sysparm_display_value', 'all');
146
-
147
- const parts = [];
148
- if (table) parts.push(`name=${table}`);
149
- if (viewSysID) parts.push(`view=${viewSysID}`);
150
- const sysparmQuery = parts.length > 0 ? `${parts.join('^')}^ORDERBYsys_created_on` : 'ORDERBYsys_created_on';
151
- params.set('sysparm_query', sysparmQuery);
152
-
153
- const records = await app.sdk.list('sys_ui_list', params);
154
- if (records.length === 0) {
155
- throw new Error(`no list layout found for ${table} with view "${viewName}"`);
156
- }
157
-
158
- const mainLayout = records[0];
159
- const layoutSysID = getStringField(mainLayout, 'sys_id');
160
-
161
- // Fetch elements (columns) for the list
162
- const elemParams = new URLSearchParams();
163
- elemParams.set('sysparm_limit', '100');
164
- elemParams.set('sysparm_fields', 'sys_id,list_id,element,position,type');
165
- elemParams.set('sysparm_display_value', 'all');
166
- elemParams.set('sysparm_query', `list_id=${layoutSysID}^ORDERBYposition`);
167
-
168
- const elemRecords = await app.sdk.list('sys_ui_list_element', elemParams);
169
-
170
- const elements = elemRecords.map(record => ({
171
- sys_id: getStringField(record, 'sys_id'),
172
- listID: getStringField(record, 'list_id'),
173
- element: getDisplayValue(record, 'element'),
174
- position: getIntValue(record, 'position'),
175
- type: getDisplayValue(record, 'type'),
176
- }));
177
-
178
- elements.sort((a, b) => a.position - b.position);
179
-
180
- // Build formatted output
181
- const lines = [];
182
- lines.push('');
183
- lines.push(chalk.bold(chalk.hex('#e8a217')(`${table} (${viewName})`)));
184
- lines.push('');
185
-
186
- lines.push(chalk.bold(chalk.hex('#666666')('─ List Columns ─')));
187
- if (elements.length === 0) {
188
- lines.push(chalk.hex('#888888')(' (no columns defined)'));
189
- } else {
190
- for (let i = 0; i < elements.length; i++) {
191
- const elem = elements[i];
192
- lines.push(` ${chalk.hex('#666666')(`${String(i + 1).padStart(2)}.`)} ${chalk.hex('#cccccc')(elem.element)}`);
193
- }
194
- }
195
- lines.push('');
196
-
197
- lines.push('─────');
198
- lines.push('');
199
- lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
200
- lines.push(` ${`jsn lists list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
201
- lines.push(` ${`jsn forms show ${table} --view "${viewName}"`.padEnd(50)} ${chalk.hex('#888888')('Show form layout')}`);
202
- lines.push('');
203
-
204
- const formatted = lines.join('\n');
205
-
206
- // Build structured data for JSON/quiet mode
207
- const columnsData = elements.map(elem => {
208
- const col = {
209
- element: elem.element,
210
- position: elem.position,
211
- };
212
- if (elem.type) {
213
- col.type = elem.type;
214
- }
215
- return col;
216
- });
217
-
218
- app.ok({
219
- table,
220
- view: viewName,
221
- columns: columnsData,
222
- _formatted: formatted,
223
- _context: {
224
- instance_url: app.getEffectiveInstance(),
225
- table: 'sys_ui_list',
226
- },
227
- }, {
228
- summary: `List: ${table} (${viewName}) - ${elements.length} columns`,
229
- breadcrumbs: [
230
- { action: 'list', cmd: `jsn lists list ${table}`, description: 'List all views' },
231
- { action: 'form', cmd: `jsn forms show ${table} --view "${viewName}"`, description: 'Show form layout' },
232
- ],
233
- });
80
+ const vp = new URLSearchParams();
81
+ vp.set('sysparm_limit', '1'); vp.set('sysparm_fields', 'sys_id');
82
+ vp.set('sysparm_query', `name=${argv.view}^name=${table}`);
83
+ const vr = await app.sdk.list('sys_ui_view', vp);
84
+ viewSysID = vr.length > 0 ? getStringField(vr[0], 'sys_id') : '';
85
+
86
+ // Find list layout
87
+ const lp = new URLSearchParams();
88
+ lp.set('sysparm_limit', '1'); lp.set('sysparm_display_value', 'all');
89
+ lp.set('sysparm_fields', 'sys_id,name,view');
90
+ lp.set('sysparm_query', `name=${table}^view=${viewSysID || argv.view}`);
91
+ const layouts = await app.sdk.list('sys_ui_list', lp);
92
+ if (layouts.length === 0) throw new Error(`No list layout for ${table} view "${argv.view}"`);
93
+ await showList(app, table, getStringField(layouts[0], 'view') || argv.view, getStringField(layouts[0], 'sys_id'));
234
94
  }),
235
95
  });
236
96
  },
237
97
  handler: () => {
238
- console.log('Manage list layouts from the sys_ui_list table.');
239
- console.log('');
240
- console.log('Available subcommands:');
241
- console.log(' list <table> List list views for a table');
242
- console.log(' show <table> Show list layout for a table');
98
+ console.log('Manage list layouts.');
243
99
  console.log('');
244
- console.log('Run "jsn lists <command> --help" for details.');
100
+ console.log('Commands:');
101
+ console.log(' list [table] List views (pick table if omitted)');
102
+ console.log(' show <table> Show list layout columns');
245
103
  },
246
104
  };
247
105
  }
106
+
107
+ async function showList(app, table, viewName, layoutSysID) {
108
+ const ep = new URLSearchParams();
109
+ ep.set('sysparm_limit', '100'); ep.set('sysparm_display_value', 'all');
110
+ ep.set('sysparm_fields', 'element,position,type');
111
+ ep.set('sysparm_query', `list_id=${layoutSysID}^ORDERBYposition`);
112
+ const elements = await app.sdk.list('sys_ui_list_element', ep);
113
+
114
+ elements.sort((a, b) => {
115
+ const pa = parseInt(getStringField(a, 'position') || '0', 10) || 0;
116
+ const pb = parseInt(getStringField(b, 'position') || '0', 10) || 0;
117
+ return pa - pb;
118
+ });
119
+
120
+ app.ok({
121
+ table,
122
+ view: viewName,
123
+ columns: elements.map(e => ({
124
+ element: getStringField(e, 'element'),
125
+ position: parseInt(getStringField(e, 'position') || '0', 10) || 0,
126
+ type: getStringField(e, 'type') || undefined,
127
+ })),
128
+ _context: { instance_url: app.getEffectiveInstance(), table: 'sys_ui_list' },
129
+ }, { summary: `${table} (${viewName}) — ${elements.length} columns` });
130
+ }
@@ -0,0 +1,28 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { getStringField } from '../../helpers.js';
3
+
4
+ export const uipoliciesCmd = (wrap) => buildDevCmd('uipolicies', 'sys_ui_policy', ['uipolicy', 'up'],
5
+ ['short_description', 'table', 'sys_class_name', 'active'], wrap, {
6
+ singular: 'UI policy',
7
+ scopeValidation: true,
8
+ onShow: async (record, app) => {
9
+ const sysID = getStringField(record, 'sys_id') || '';
10
+ if (!sysID) return;
11
+ const mp = new URLSearchParams();
12
+ mp.set('sysparm_query', `ui_policy=${sysID}^ORDERBYorder`);
13
+ mp.set('sysparm_limit', '100'); mp.set('sysparm_display_value', 'all');
14
+ mp.set('sysparm_fields', 'name,table,field_name,read_only,mandatory,visible,sys_id');
15
+ const actions = await app.sdk.list('sys_ui_policy_action', mp);
16
+ if (actions.length > 0) {
17
+ console.log(` Actions (${actions.length}):`);
18
+ for (const a of actions) {
19
+ const f = getStringField(a, 'field_name') || '(all fields)';
20
+ const flags = [];
21
+ if (getStringField(a, 'mandatory') === 'true') flags.push('mandatory');
22
+ if (getStringField(a, 'read_only') === 'true') flags.push('read-only');
23
+ if (getStringField(a, 'visible') === 'false') flags.push('hidden');
24
+ console.log(` ${f} ${flags.length ? `[${flags.join(', ')}]` : ''}`);
25
+ }
26
+ }
27
+ },
28
+ });
@@ -7,7 +7,7 @@ import {
7
7
  spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
8
8
  aclsCmd, rolesCmd, propertiesCmd,
9
9
  relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
10
- privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
10
+ privilegesCmd, uxscriptsCmd, aliasesCmd,
11
11
  catalogscriptsCmd, cataloguipoliciesCmd,
12
12
  } from './dev/_simple.js';
13
13
  import { flowsCmd } from './dev/flows.js';
@@ -53,7 +53,6 @@ export function devCmd(wrap) {
53
53
  .command(listcontrolsCmd(wrap))
54
54
  .command(viewsCmd(wrap))
55
55
  .command(privilegesCmd(wrap))
56
- .command(securitytypesCmd(wrap))
57
56
  .command(uxscriptsCmd(wrap))
58
57
  .command(aliasesCmd(wrap))
59
58
  .command(catalogscriptsCmd(wrap))
@@ -24,8 +24,30 @@ export function groupsCmd(wrap) {
24
24
  formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'manager') ? '→ ' + getStringField(r, 'manager') : ''}`,
25
25
  });
26
26
  if (picked) {
27
+ const sysID = getStringField(picked, 'sys_id') || '';
28
+ // Fetch member + role counts for hints
29
+ let memberCount = 0, roleCount = 0;
30
+ try {
31
+ const cp = new URLSearchParams();
32
+ cp.set('sysparm_query', `group=${sysID}`); cp.set('sysparm_limit', '0'); cp.set('sysparm_fields', 'sys_id');
33
+ const members = await app.sdk.list('sys_user_grmember', cp);
34
+ memberCount = members.length;
35
+ } catch {}
36
+ try {
37
+ const rp = new URLSearchParams();
38
+ rp.set('sysparm_query', `group=${sysID}`); rp.set('sysparm_limit', '0'); rp.set('sysparm_fields', 'sys_id');
39
+ const roles = await app.sdk.list('sys_group_has_role', rp);
40
+ roleCount = roles.length;
41
+ } catch {}
42
+
27
43
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_user_group' };
28
- return app.ok(picked, { summary: `Group: ${getStringField(picked, 'name')}` });
44
+ return app.ok(picked, {
45
+ summary: `Group: ${getStringField(picked, 'name')}`,
46
+ breadcrumbs: [
47
+ memberCount > 0 ? { action: 'members', cmd: `jsn records list --table sys_user_grmember --query "group=${sysID}"`, description: `${memberCount} member(s)` } : null,
48
+ roleCount > 0 ? { action: 'roles', cmd: `jsn records list --table sys_group_has_role --query "group=${sysID}"`, description: `${roleCount} role(s)` } : null,
49
+ ].filter(Boolean),
50
+ });
29
51
  }
30
52
 
31
53
  const params = new URLSearchParams();
@@ -24,8 +24,15 @@ export function usersCmd(wrap) {
24
24
  formatLabel: r => `${getStringField(r, 'user_name')} (${getStringField(r, 'name') || '-'})`,
25
25
  });
26
26
  if (picked) {
27
+ const sysID = getStringField(picked, 'sys_id') || '';
27
28
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_user' };
28
- return app.ok(picked, { summary: `User: ${getStringField(picked, 'user_name')}` });
29
+ return app.ok(picked, {
30
+ summary: `User: ${getStringField(picked, 'user_name')}`,
31
+ breadcrumbs: [
32
+ { action: 'preferences', cmd: `jsn records list --table sys_user_preference --query "user=${sysID}"`, description: 'User preferences' },
33
+ { action: 'groups', cmd: `jsn records list --table sys_user_grmember --query "user=${sysID}"`, description: 'Group memberships' },
34
+ ],
35
+ });
29
36
  }
30
37
 
31
38
  const params = new URLSearchParams();
package/src/help.js CHANGED
@@ -53,7 +53,6 @@ const ACCESS_COMMANDS = [
53
53
  { name: 'b4rules', alias: 'b4r', desc: 'Before-query business rules (see also Automation)' },
54
54
  { name: 'roles', alias: 'role', desc: 'Roles' },
55
55
  { name: 'privileges', alias: 'priv', desc: 'Scope privileges' },
56
- { name: 'securitytypes', alias: 'st', desc: 'Custom security types' },
57
56
  { name: 'aliases', alias: 'als', desc: 'Connection & credential aliases' },
58
57
  { name: 'groups', alias: 'group', desc: 'Manage groups' },
59
58
  { name: 'groupmembers', alias: 'gm', desc: 'Group memberships' },
package/src/output.js CHANGED
@@ -460,6 +460,9 @@ export function getTableURL(instance, table, sysID) {
460
460
  sysevent_email_action: `nav_to.do?uri=sysevent_email_action.do?sys_id=${sysID}`,
461
461
  sysevent_in_email_action: `nav_to.do?uri=sysevent_in_email_action.do?sys_id=${sysID}`,
462
462
  soap_message: `nav_to.do?uri=soap_message.do?sys_id=${sysID}`,
463
+ sys_security_acl: `nav_to.do?uri=sys_security_acl.do?sys_id=${sysID}`,
464
+ sys_user_role: `nav_to.do?uri=sys_user_role.do?sys_id=${sysID}`,
465
+ sys_db_object: `nav_to.do?uri=sys_db_object.do?sys_id=${sysID}`,
463
466
  };
464
467
  const path = mapped[table] || `${table}.do?sys_id=${sysID}`;
465
468
  return `${instance}/${path}`;