@jacebenson/jsn 3.1.0 → 3.1.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.
@@ -117,6 +117,17 @@ export function ticketsCmd(wrap) {
117
117
  })
118
118
 
119
119
  },
120
- handler: () => {},
120
+ handler: () => {
121
+ console.log('Query generic tickets from the ticket table.');
122
+ console.log('');
123
+ console.log('Available subcommands:');
124
+ console.log(' list List tickets');
125
+ console.log(' show <number or sys_id> Show a ticket');
126
+ console.log(' create Create a new ticket');
127
+ console.log(' update <identifier> Update a ticket');
128
+ console.log(' delete <identifier> Delete a ticket');
129
+ console.log('');
130
+ console.log('Run "jsn tickets <command> --help" for details.');
131
+ },
121
132
  };
122
133
  }
@@ -140,6 +140,17 @@ export function usersCmd(wrap) {
140
140
  }),
141
141
  });
142
142
  },
143
- handler: () => {},
143
+ handler: () => {
144
+ console.log('Manage ServiceNow users from the sys_user table.');
145
+ console.log('');
146
+ console.log('Available subcommands:');
147
+ console.log(' list List users');
148
+ console.log(' show <name or sys_id> Show a user by user_name or sys_id');
149
+ console.log(' create Create a new user');
150
+ console.log(' update <identifier> Update a user');
151
+ console.log(' delete <identifier> Delete a user');
152
+ console.log('');
153
+ console.log('Run "jsn users <command> --help" for details.');
154
+ },
144
155
  };
145
156
  }
package/src/help.js CHANGED
@@ -1,87 +1,140 @@
1
1
  // Custom grouped help renderer
2
- // Mirrors the Go version's custom Cobra help template
3
-
4
- const COMMAND_GROUPS = {
5
- 'CORE COMMANDS': [
6
- { name: 'incidents', alias: 'inc', desc: 'Manage incidents' },
7
- { name: 'changes', alias: 'chg', desc: 'Manage change requests' },
8
- { name: 'requests', alias: 'req', desc: 'Manage service catalog requests' },
9
- { name: 'tasks', alias: 'task', desc: 'Manage catalog tasks' },
10
- { name: 'tickets', alias: 'ticket', desc: 'Query generic tickets' },
11
- ],
12
- 'AUTOMATION': [
13
- { name: 'flows', desc: 'Manage Flow Designer flows' },
14
- { name: 'actions', alias: 'action', desc: 'Manage flow actions (sys_hub_action_type_definition)' },
15
- { name: 'rules', alias: 'br', desc: 'Manage business rules' },
16
- { name: 'scrapi', desc: 'Manage Scripted REST APIs' },
17
- { name: 'updatesets', desc: 'Manage update sets (set, yolo, export, import)' },
18
- { name: 'eval', desc: 'Run server-side JavaScript' },
19
- { name: 'rest', desc: 'Make raw REST API calls' },
20
- ],
21
- 'ACCESS': [
22
- { name: 'acls', alias: 'acl', desc: 'Manage access controls' },
23
- { name: 'roles', alias: 'role', desc: 'Manage roles' },
24
- { name: 'scopes', desc: 'Manage application scopes' },
25
- { name: 'properties', alias: 'prop', desc: 'Manage system properties' },
26
- { name: 'privileges', alias: 'priv', desc: 'Manage scope privileges' },
27
- { name: 'securitytypes', alias: 'st', desc: 'Manage security types' },
28
- { name: 'aliases', alias: 'als', desc: 'Manage table aliases' },
29
- ],
30
- 'USER EXPERIENCE': [
31
- { name: 'forms', desc: 'Manage form layouts' },
32
- { name: 'lists', desc: 'Manage list layouts' },
33
- { name: 'clientscripts', alias: 'cs', desc: 'Manage client scripts' },
34
- { name: 'uipolicies', alias: 'up', desc: 'Manage UI policies' },
35
- { name: 'uiactions', alias: 'ua', desc: 'Manage UI actions' },
36
- { name: 'sppages', desc: 'Manage Service Portal pages' },
37
- { name: 'spwidgets', desc: 'Manage Service Portal widgets' },
38
- { name: 'uipages', desc: 'Manage UI pages' },
39
- { name: 'appmenu', desc: 'Manage application menus' },
40
- { name: 'listcontrols', alias: 'lc', desc: 'Manage list controls' },
41
- { name: 'views', alias: 'vw', desc: 'Manage views' },
42
- { name: 'uxscripts', alias: 'ux', desc: 'Manage UX source scripts' },
43
- { name: 'catalogscripts', desc: 'Manage catalog client scripts' },
44
- { name: 'cataloguipolicies', alias: 'cup', desc: 'Manage catalog UI policies' },
45
- ],
46
- 'DATA': [
47
- { name: 'records', desc: 'Generic Table API for any table' },
48
- { name: 'tables', alias: 't', desc: 'Manage table definitions (sys_db_object)' },
49
- { name: 'columns', alias: 'col', desc: 'Manage column definitions (sys_dictionary)' },
50
- { name: 'includes', alias: 'si', desc: 'Manage script includes' },
51
- { name: 'import', desc: 'Manage import sets' },
52
- { name: 'logs', desc: 'View system logs' },
53
- { name: 'users', alias: 'user', desc: 'Manage ServiceNow users' },
54
- { name: 'groups', alias: 'group', desc: 'Manage groups' },
55
- { name: 'groupmembers', alias: 'gm', desc: 'Manage group memberships' },
56
- { name: 'grouproles', alias: 'gr', desc: 'Manage group roles' },
57
- { name: 'relationships', alias: 'rel', desc: 'Manage table relationships' },
58
- { name: 'appmodules', alias: 'am', desc: 'Manage application modules' },
59
- ],
60
- };
61
-
62
- const CONFIGURATION_COMMANDS = [
63
- { name: 'setup', desc: 'Interactive first-time setup' },
64
- { name: 'auth', desc: 'Manage OAuth authentication' },
65
- { name: 'profiles', alias: 'profile', desc: 'Manage instance profiles' },
66
- { name: 'dev', desc: '[DEPRECATED] All dev subcommands are now top-level — use jsn flows, jsn acls, etc.' },
67
- ];
68
-
69
- const UTILITY_COMMANDS = [
70
- { name: 'skill', desc: 'Manage the jsn AI agent skill file (show, fetch, install)' },
71
- { name: 'version', desc: 'Show version information (--check for npm updates)' },
72
- ];
2
+ // Organized by ServiceNow domain model with subgroup dividers
73
3
 
74
4
  function renderGroup(name, commands) {
75
5
  const lines = [`\n${name}`];
76
6
  lines.push('─'.repeat(50));
77
7
  for (const cmd of commands) {
8
+ if (cmd.divider) {
9
+ lines.push('');
10
+ lines.push(` ${cmd.divider}`);
11
+ continue;
12
+ }
78
13
  const aliasPart = cmd.alias ? ` (${cmd.alias})` : '';
79
14
  const padded = ` jsn ${cmd.name}`.padEnd(22);
80
- lines.push(`${padded}${cmd.desc}${aliasPart}`);
15
+ const status = cmd.status ? ` ${cmd.status}` : '';
16
+ lines.push(`${padded}${cmd.desc}${aliasPart}${status}`);
81
17
  }
82
18
  return lines.join('\n');
83
19
  }
84
20
 
21
+ const CORE_COMMANDS = [
22
+ { name: 'incidents', alias: 'inc', desc: 'Manage incidents' },
23
+ { name: 'changes', alias: 'chg', desc: 'Manage change requests' },
24
+ { name: 'requests', alias: 'req', desc: 'Manage service catalog requests' },
25
+ { name: 'tasks', alias: 'task', desc: 'Manage catalog tasks' },
26
+ ];
27
+
28
+ const AUTOMATION_COMMANDS = [
29
+ { divider: '── Async ──' },
30
+ { name: 'flows', alias: 'flow', desc: 'Flow Designer flows' },
31
+ { name: 'actions', alias: 'action', desc: 'Flow action definitions' },
32
+ { name: 'scriptactions', desc: 'Script actions' },
33
+ { name: 'scheduledjobs', desc: 'Scheduled jobs' },
34
+ { name: 'triggers', desc: 'Event triggers' },
35
+ { name: 'asyncrules', desc: 'Asynchronous business rules' },
36
+
37
+ { divider: '── In Memory ──' },
38
+ { name: 'rules', alias: 'br', desc: 'Business rules (create, update, delete)' },
39
+ { name: 'workflows', desc: 'Workflow definitions' },
40
+ { name: 'decisiontables', desc: 'Decision tables' },
41
+ { name: 'assignments', desc: 'Assignment rules' },
42
+
43
+ { divider: '── Inbound ──' },
44
+ { name: 'scrapi', desc: 'Scripted REST APIs' },
45
+ { name: 'email', desc: 'Inbound email actions' },
46
+
47
+ { divider: '── Outbound ──' },
48
+ { name: 'restmessage', alias: 'rm', desc: 'REST Messages (sys_rest_message)' },
49
+ { name: 'restmethods', desc: 'REST Message methods (sys_rest_message_fn)' },
50
+ { name: 'soapmessages', desc: 'SOAP Messages' },
51
+ { name: 'soapfunctions', desc: 'SOAP Message functions' },
52
+ ];
53
+
54
+ const ACCESS_COMMANDS = [
55
+ { name: 'acls', alias: 'acl', desc: 'Access controls' },
56
+ { name: 'b4rules', alias: 'b4r', desc: 'Before-query business rules (see also Automation)' },
57
+ { name: 'roles', alias: 'role', desc: 'Roles' },
58
+ { name: 'privileges', alias: 'priv', desc: 'Scope privileges' },
59
+ { name: 'securitytypes', alias: 'st', desc: 'Custom security types' },
60
+ { name: 'aliases', alias: 'als', desc: 'Connection & credential aliases' },
61
+ { name: 'groups', alias: 'group', desc: 'Manage groups' },
62
+ { name: 'groupmembers', alias: 'gm', desc: 'Group memberships' },
63
+ { name: 'grouproles', alias: 'gr', desc: 'Group roles' },
64
+ { name: 'users', alias: 'user', desc: 'Manage users' },
65
+ ];
66
+
67
+ const UX_COMMANDS = [
68
+ { divider: '── Shared (across all experiences) ──' },
69
+ { name: 'forms', desc: 'Form layouts' },
70
+ { name: 'lists', desc: 'List layouts' },
71
+ { name: 'clientscripts', alias: 'cs', desc: 'Client scripts' },
72
+ { name: 'uipolicies', alias: 'up', desc: 'UI policies' },
73
+ { name: 'uiactions', alias: 'ua', desc: 'UI actions (buttons, links, context menus)' },
74
+ { name: 'views', alias: 'vw', desc: 'Views' },
75
+ { name: 'catalogitems', alias: 'ci', desc: 'Service Catalog items + variables' },
76
+ { name: 'catalogscripts', desc: 'Catalog client scripts' },
77
+ { name: 'cataloguipolicies', alias: 'cup', desc: 'Catalog UI policies' },
78
+
79
+ { divider: '── Core UI ──' },
80
+ { name: 'uipages', desc: 'UI pages' },
81
+ { name: 'uimacros', desc: 'UI macros' },
82
+ { name: 'listcontrols', alias: 'lc', desc: 'List controls' },
83
+
84
+ { divider: '── Service Portal ──' },
85
+ { name: 'sppages', desc: 'Service Portal pages' },
86
+ { name: 'spwidgets', desc: 'Service Portal widgets' },
87
+
88
+ { divider: '── Next Experience (Workspaces) ──' },
89
+ { name: 'uxscripts', alias: 'ux', desc: 'UX source scripts' },
90
+ { name: 'uxlists', desc: 'Workspace lists (sys_ux_list)' },
91
+ { name: 'uxapplicability', desc: 'Workspace applicability (m2m)' },
92
+
93
+ { divider: '── Navigation ──' },
94
+ { name: 'appmenu', desc: 'Application menus' },
95
+ { name: 'appmodules', alias: 'am', desc: 'Application modules' },
96
+ ];
97
+
98
+ const DATA_COMMANDS = [
99
+ { divider: '── DB Schema ──' },
100
+ { name: 'tables', alias: 't', desc: 'Table definitions' },
101
+ { name: 'columns', alias: 'col', desc: 'Column definitions' },
102
+ { name: 'relationships', alias: 'rel', desc: 'Table relationships' },
103
+
104
+ { divider: '── Shared Code, Transforms, Logs ──' },
105
+ { name: 'includes', alias: 'si', desc: 'Script includes' },
106
+ { name: 'import', desc: 'Import sets' },
107
+ { name: 'logs', desc: 'System logs' },
108
+ { name: 'properties', alias: 'prop', desc: 'System properties' },
109
+
110
+ { name: 'records', desc: 'Generic Table API (any table)' },
111
+ ];
112
+
113
+ const CONFIG_COMMANDS = [
114
+ { name: 'setup', desc: 'Interactive first-time setup' },
115
+ { name: 'auth', desc: 'OAuth authentication' },
116
+ { name: 'profiles', alias: 'profile', desc: 'Instance profiles' },
117
+ { name: 'updatesets', desc: 'Update sets (set, yolo, export, import)' },
118
+ { name: 'scopes', desc: 'Application scopes' },
119
+ ];
120
+
121
+ const DEVELOPER_COMMANDS = [
122
+ { name: 'eval', desc: 'Run server-side JavaScript' },
123
+ { name: 'rest', desc: 'Raw REST API calls' },
124
+ { name: 'skill', desc: 'AI agent skill file (show, fetch, install)' },
125
+ { name: 'version', desc: 'Version info (--check for npm updates)' },
126
+ ];
127
+
128
+ const COMMAND_GROUPS = {
129
+ 'CORE': CORE_COMMANDS,
130
+ 'AUTOMATION': AUTOMATION_COMMANDS,
131
+ 'ACCESS': ACCESS_COMMANDS,
132
+ 'USER EXPERIENCE': UX_COMMANDS,
133
+ 'DATA': DATA_COMMANDS,
134
+ 'CONFIGURATION': CONFIG_COMMANDS,
135
+ 'DEVELOPER': DEVELOPER_COMMANDS,
136
+ };
137
+
85
138
  function renderFlags() {
86
139
  return `
87
140
  FLAGS
@@ -107,8 +160,6 @@ LEARN MORE
107
160
 
108
161
  export function renderHelp() {
109
162
  const sections = Object.entries(COMMAND_GROUPS).map(([name, cmds]) => renderGroup(name, cmds));
110
- const configSection = renderGroup('CONFIGURATION', CONFIGURATION_COMMANDS);
111
- const utilitySection = renderGroup('UTILITY', UTILITY_COMMANDS);
112
163
 
113
164
  return [
114
165
  'Usage: jsn <command> [options]',
@@ -117,10 +168,6 @@ export function renderHelp() {
117
168
  '',
118
169
  ...sections,
119
170
  '',
120
- configSection,
121
- '',
122
- utilitySection,
123
- '',
124
171
  renderFlags(),
125
172
  renderTips(),
126
173
  '',
package/src/helpers.js CHANGED
@@ -89,10 +89,15 @@ export function resolveFieldsParam(columns) {
89
89
  * @param {string} opts.labelField — field used to match selection (default: 'name')
90
90
  * @returns {Promise<void>|null} null if no selection made or non-interactive
91
91
  */
92
+ function vowelArticle(word) {
93
+ const first = word.charAt(0).toLowerCase();
94
+ return first === 'a' || first === 'e' || first === 'i' || first === 'o' || first === 'u' ? 'an' : 'a';
95
+ }
96
+
92
97
  export async function interactiveList({ app, table, singular, columns, limit = 50, query = '', formatLabel, labelField = 'name' }) {
93
98
  app.requireInstance();
94
99
  const effectiveFormat = app.output.getFormat() === FormatAuto ? (isTTY(process.stdout) ? FormatAuto : FormatAuto) : app.output.getFormat();
95
- if (effectiveFormat !== FormatAuto || !isTTY(process.stdout) || !isTTY(process.stdin) || query) {
100
+ if (effectiveFormat !== FormatAuto || !isTTY(process.stdout) || !isTTY(process.stdin)) {
96
101
  return null; // not interactive — caller should fall back to text/table
97
102
  }
98
103
 
@@ -108,23 +113,32 @@ export async function interactiveList({ app, table, singular, columns, limit = 5
108
113
  }
109
114
  if (totalCount === 0) return null;
110
115
 
111
- // Server source adapter for paginatedSearch: (term, offset) => choices[]
112
- async function serverSource(term, offset) {
113
- const params = new URLSearchParams();
114
- params.set('sysparm_limit', String(limit));
115
- params.set('sysparm_display_value', 'all');
116
- if (pickerFields) params.set('sysparm_fields', pickerFields);
117
- if (term) {
118
- params.set('sysparm_query', `${labelField}LIKE${term}^ORDERBYDESCsys_updated_on`);
119
- } else {
120
- params.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
121
- params.set('sysparm_offset', String(offset));
116
+ // Paginated source adapter: (term, offset, { signal }) => choices[]
117
+ async function serverSource(term, offset, { signal } = {}) {
118
+ try {
119
+ const params = new URLSearchParams();
120
+ params.set('sysparm_limit', String(limit));
121
+ params.set('sysparm_display_value', 'all');
122
+ if (pickerFields) params.set('sysparm_fields', pickerFields);
123
+ if (term) {
124
+ params.set('sysparm_query', `${labelField}LIKE${term}^ORDERBYDESCsys_updated_on`);
125
+ } else {
126
+ params.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
127
+ params.set('sysparm_offset', String(offset));
128
+ }
129
+ const records = await app.sdk.list(table, params);
130
+ if (!Array.isArray(records)) {
131
+ console.error('SDK non-array:', typeof records);
132
+ return [];
133
+ }
134
+ return records.map(r => ({
135
+ name: formatLabel ? formatLabel(r) : (getStringField(r, labelField) || getStringField(r, 'sys_id')),
136
+ value: r,
137
+ }));
138
+ } catch (err) {
139
+ console.error('serverSource error:', err.message || err);
140
+ return [];
122
141
  }
123
- const records = await app.sdk.list(table, params);
124
- return records.map(r => ({
125
- name: formatLabel ? formatLabel(r) : (getStringField(r, labelField) || getStringField(r, 'sys_id')),
126
- value: r,
127
- }));
128
142
  }
129
143
 
130
144
  const selected = await paginatedSearch({
@@ -133,9 +147,8 @@ export async function interactiveList({ app, table, singular, columns, limit = 5
133
147
  totalCount,
134
148
  source: serverSource,
135
149
  });
136
- // null = cancelled → undefined so callers skip fallback table
137
- if (selected === null) return undefined;
138
- return selected?.value; // unwrap {name, value} → raw record
150
+
151
+ return selected?.value; // unwrap {name, value} → raw record, undefined on cancel
139
152
  }
140
153
 
141
154
  /**