@jacebenson/jsn 3.1.0 → 3.1.2

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.
@@ -97,4 +97,31 @@ export const securitytypesCmd = (wrap) => buildDevCmd('securitytypes', 'sys_secu
97
97
  export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
98
98
  export const aliasesCmd = (wrap) => buildDevCmd('aliases', 'sys_alias', ['alias', 'als'], ['name', 'table', 'sys_scope'], wrap, { singular: 'alias', scopeValidation: true });
99
99
  export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
100
+
101
+ // ── Automation: Async ──
102
+ export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
103
+ export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
104
+ export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'order>=1000' });
105
+ export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
106
+
107
+ // ── Automation: In Memory ──
108
+ export const workflowsCmd = (wrap) => buildDevCmd('workflows', 'wf_workflow', ['workflow'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'workflow', scopeValidation: true, readOnly: true });
109
+ export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_decision', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
110
+ export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
111
+
112
+ // ── Automation: Inbound ──
113
+ export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_email_action', ['emails'], ['name', 'active', 'event_name', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
114
+
115
+ // ── Automation: Outbound ──
116
+ export const restmessageCmd = (wrap) => buildDevCmd('restmessage', 'sys_rest_message', ['rm', 'restmsg'], ['name', 'endpoint', 'active', 'sys_scope'], wrap, { singular: 'REST message', scopeValidation: true });
117
+ export const restmethodsCmd = (wrap) => buildDevCmd('restmethods', 'sys_rest_message_fn', ['restmethod'], ['name', 'http_method', 'active', 'sys_scope'], wrap, { singular: 'REST method', scopeValidation: true });
118
+ export const soapmessagesCmd = (wrap) => buildDevCmd('soapmessages', 'sys_soap_message', ['soapmsg'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP message', scopeValidation: true });
119
+ export const soapfunctionsCmd = (wrap) => buildDevCmd('soapfunctions', 'sys_soap_message_function', ['soapfunc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP function', scopeValidation: true });
120
+
121
+ // ── UX: Core UI ──
122
+ export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
123
+
124
+ // ── UX: Workspaces ──
125
+ export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
126
+ export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
100
127
  export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
@@ -0,0 +1,5 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+
3
+ // Before-query business rules — applies filters to GlideRecord queries before they execute.
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' });
@@ -1,7 +1,4 @@
1
- import chalk from 'chalk';
2
- import { getStringField } from '../../helpers.js';
3
-
4
- // ─── Helpers ───
1
+ import { getStringField, interactiveList } from '../../helpers.js';
5
2
 
6
3
  function getDisplayValue(record, key) {
7
4
  if (!record || typeof record !== 'object') return '';
@@ -20,14 +17,8 @@ function getIntValue(record, key) {
20
17
  const val = record[key];
21
18
  if (val == null) return 0;
22
19
  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
- }
20
+ if (typeof val === 'string') { const n = parseInt(val, 10); return isNaN(n) ? 0 : n; }
21
+ if (typeof val === 'object' && val.value != null) { const n = parseInt(val.value, 10); return isNaN(n) ? 0 : n; }
31
22
  return 0;
32
23
  }
33
24
 
@@ -40,7 +31,87 @@ function getBoolValue(record, key) {
40
31
  return false;
41
32
  }
42
33
 
43
- // ─── Commands ───
34
+ async function showForm(app, table, viewName) {
35
+ // 1) Look up the view
36
+ const viewParams = new URLSearchParams();
37
+ viewParams.set('sysparm_limit', '1');
38
+ viewParams.set('sysparm_fields', 'sys_id');
39
+ viewParams.set('sysparm_display_value', 'all');
40
+ viewParams.set('sysparm_query', `name=${viewName}`);
41
+ let viewSysID = '';
42
+ try { const vr = await app.sdk.list('sys_ui_view', viewParams); if (vr.length > 0) viewSysID = getStringField(vr[0], 'sys_id'); } catch { /* ignore */ }
43
+ if (!viewSysID) {
44
+ viewParams.set('sysparm_query', `title=${viewName}`);
45
+ try { const vr = await app.sdk.list('sys_ui_view', viewParams); if (vr.length > 0) viewSysID = getStringField(vr[0], 'sys_id'); } catch { /* ignore */ }
46
+ }
47
+
48
+ // 2) Get sections for this table + view
49
+ const secParams = new URLSearchParams();
50
+ secParams.set('sysparm_limit', '200');
51
+ secParams.set('sysparm_fields', 'sys_id,caption');
52
+ secParams.set('sysparm_display_value', 'all');
53
+ secParams.set('sysparm_query', `name=${table}^view=${viewName}^ORDERBYposition`);
54
+ if (viewSysID) {
55
+ secParams.set('sysparm_query', `name=${table}^view=${viewSysID}^ORDERBYposition`);
56
+ }
57
+ const sections = await app.sdk.list('sys_ui_section', secParams);
58
+
59
+ // 3) Get elements for each section
60
+ const sectionsOut = [];
61
+ for (const sec of sections) {
62
+ const secSysID = getStringField(sec, 'sys_id');
63
+ const sectionCaption = getDisplayValue(sec, 'caption') || '(unnamed)';
64
+
65
+ let elements = [];
66
+ if (secSysID) {
67
+ const elemParams = new URLSearchParams();
68
+ elemParams.set('sysparm_limit', '500');
69
+ elemParams.set('sysparm_fields', 'element,type,label,position');
70
+ elemParams.set('sysparm_display_value', 'all');
71
+ elemParams.set('sysparm_query', `sys_ui_section=${secSysID}^ORDERBYposition`);
72
+ try { elements = await app.sdk.list('sys_ui_element', elemParams); } catch { /* ignore */ }
73
+ }
74
+
75
+ sectionsOut.push({
76
+ caption: sectionCaption,
77
+ position: getIntValue(sec, 'position'),
78
+ elements: elements
79
+ .map(e => ({
80
+ type: getDisplayValue(e, 'type'),
81
+ label: getDisplayValue(e, 'label'),
82
+ element: getDisplayValue(e, 'element'),
83
+ position: getIntValue(e, 'position'),
84
+ }))
85
+ .sort((a, b) => a.position - b.position),
86
+ });
87
+ }
88
+
89
+ sectionsOut.sort((a, b) => a.position - b.position);
90
+
91
+ const totalElements = sectionsOut.reduce((sum, s) => sum + s.elements.length, 0);
92
+
93
+ // Build formatted output
94
+ const lines = [];
95
+ lines.push(`Form: ${table} → ${viewName}`);
96
+ if (viewSysID) lines.push(` View: ${app.getEffectiveInstance()}/nav_to.do?uri=sys_ui_view.do?sys_id=${viewSysID}`);
97
+ lines.push('');
98
+
99
+ for (const sec of sectionsOut) {
100
+ const secLabel = sec.caption || '(unnamed)';
101
+ lines.push(`── ${secLabel} ──`);
102
+ if (sec.elements.length === 0) {
103
+ lines.push(' (no elements)');
104
+ }
105
+ for (const el of sec.elements) {
106
+ const elLabel = el.label || el.element || '(unnamed)';
107
+ const typeStr = el.type ? ` (${el.type})` : '';
108
+ lines.push(` ${elLabel}${typeStr}`);
109
+ }
110
+ lines.push('');
111
+ }
112
+
113
+ process.stdout.write(lines.join('\n') + '\n');
114
+ }
44
115
 
45
116
  export function formsCmd(wrap) {
46
117
  return {
@@ -50,58 +121,52 @@ export function formsCmd(wrap) {
50
121
  builder: (yargs) => {
51
122
  return yargs
52
123
  .command({
53
- command: 'list <table>',
124
+ command: 'list [table]',
54
125
  aliases: ['ls'],
55
- describe: 'List form views for a table',
126
+ describe: 'List form views',
56
127
  builder: (y) => y
57
- .positional('table', {
58
- describe: 'Table name (e.g. incident, change_request)',
59
- type: 'string',
60
- })
128
+ .positional('table', { describe: 'Table name (e.g. incident, change_request)', type: 'string' })
61
129
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
62
130
  handler: wrap(async (argv, app) => {
63
131
  app.requireInstance();
64
- const table = argv.table;
65
- const limit = argv.limit || 50;
132
+ const table = argv.table || '';
133
+ const query = table ? `name=${table}` : '';
134
+
135
+ const picked = await interactiveList({
136
+ app, table: 'sys_ui_form', singular: 'form', columns: ['name', 'view'], limit: argv.limit, query, labelField: 'name',
137
+ formatLabel: r => {
138
+ const t = getStringField(r, 'name') || '';
139
+ const v = getStringField(r, 'view') || '';
140
+ return `${t} — ${v}`;
141
+ },
142
+ });
143
+ if (picked === undefined) return;
144
+ if (picked) {
145
+ const t = getStringField(picked, 'name') || '';
146
+ const v = getStringField(picked, 'view') || 'Default view';
147
+ // Show full form layout using the show logic
148
+ await showForm(app, t, v);
149
+ return;
150
+ }
66
151
 
152
+ // Fallback
67
153
  const params = new URLSearchParams();
68
- params.set('sysparm_limit', String(limit));
69
- params.set('sysparm_fields', 'view');
70
- params.set('sysparm_group_by', 'view');
154
+ params.set('sysparm_limit', String(argv.limit));
155
+ params.set('sysparm_fields', 'name,view,sys_id');
71
156
  params.set('sysparm_display_value', 'all');
72
- const sysparmQuery = table ? `name=${table}^ORDERBYview` : 'ORDERBYview';
73
- params.set('sysparm_query', sysparmQuery);
74
-
75
- const records = await app.sdk.list('sys_ui_section', params);
76
-
77
- const viewMap = new Map();
78
- for (const record of records) {
79
- const view = getDisplayValue(record, 'view');
80
- if (view && !viewMap.has(view)) {
81
- viewMap.set(view, true);
82
- }
83
- }
84
-
85
- const views = Array.from(viewMap.keys()).sort();
86
- const defaultViews = views.filter(v => v === 'Default view');
87
- const workspaceViews = views.filter(v => v.toLowerCase().includes('workspace'));
88
- const otherViews = views.filter(v => !defaultViews.includes(v) && !workspaceViews.includes(v));
89
-
90
- const recordsOut = views.map(v => ({ view: v, table }));
157
+ params.set('sysparm_query', (query || '') + '^ORDERBYname');
158
+ const records = await app.sdk.list('sys_ui_form', params);
91
159
 
160
+ const desc = table ? `for ${table}` : 'across all tables';
92
161
  app.ok({
93
- table,
94
- count: views.length,
95
- default: defaultViews,
96
- workspaces: workspaceViews,
97
- other: otherViews,
98
- records: recordsOut,
99
- instance_url: app.getEffectiveInstance(),
162
+ table: table || 'sys_ui_form',
163
+ count: records.length,
164
+ columns: ['name', 'view'],
165
+ records,
166
+ context: { instance_url: app.getEffectiveInstance() },
100
167
  }, {
101
- summary: `${views.length} views for ${table}`,
102
- breadcrumbs: [
103
- { action: 'show', cmd: `jsn forms show ${table} --view "Default view"`, description: 'Show Default view layout' },
104
- ],
168
+ summary: `${records.length} form(s) ${desc}`,
169
+ ...(table ? { breadcrumbs: [{ action: 'show', cmd: `jsn forms show ${table} --view "Default view"`, description: 'Show Default view layout' }] } : {}),
105
170
  });
106
171
  }),
107
172
  })
@@ -113,213 +178,16 @@ export function formsCmd(wrap) {
113
178
  .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
114
179
  handler: wrap(async (argv, app) => {
115
180
  app.requireInstance();
116
- const table = argv.table;
117
- const viewName = argv.view;
118
-
119
- // Look up view sys_id
120
- let viewSysID = '';
121
- const viewParams = new URLSearchParams();
122
- viewParams.set('sysparm_limit', '1');
123
- viewParams.set('sysparm_fields', 'sys_id');
124
- viewParams.set('sysparm_query', `name=${viewName}`);
125
- try {
126
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
127
- if (viewRecords.length > 0) {
128
- viewSysID = getStringField(viewRecords[0], 'sys_id');
129
- }
130
- } catch {
131
- // ignore
132
- }
133
-
134
- if (!viewSysID) {
135
- viewParams.set('sysparm_query', `title=${viewName}`);
136
- try {
137
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
138
- if (viewRecords.length > 0) {
139
- viewSysID = getStringField(viewRecords[0], 'sys_id');
140
- }
141
- } catch {
142
- // ignore
143
- }
144
- }
145
-
146
- if (!viewSysID) {
147
- viewSysID = viewName;
148
- }
149
-
150
- // Fetch sections
151
- const params = new URLSearchParams();
152
- params.set('sysparm_limit', '100');
153
- params.set('sysparm_fields', 'sys_id,name,view,caption,header,order,active,sys_created_on,sys_updated_on');
154
- params.set('sysparm_display_value', 'all');
155
-
156
- const parts = [];
157
- if (table) parts.push(`name=${table}`);
158
- if (viewSysID) parts.push(`view=${viewSysID}`);
159
- const sysparmQuery = parts.length > 0 ? `${parts.join('^')}^ORDERBYorder` : 'ORDERBYorder';
160
- params.set('sysparm_query', sysparmQuery);
161
-
162
- const records = await app.sdk.list('sys_ui_section', params);
163
- if (records.length === 0) {
164
- throw new Error(`no form sections found for ${table} with view "${viewName}"`);
165
- }
166
-
167
- // Parse sections
168
- const sections = records.map(record => ({
169
- sys_id: getStringField(record, 'sys_id'),
170
- name: getDisplayValue(record, 'name'),
171
- view: getDisplayValue(record, 'view'),
172
- caption: getDisplayValue(record, 'caption'),
173
- header: getDisplayValue(record, 'header'),
174
- order: getIntValue(record, 'order'),
175
- active: getBoolValue(record, 'active'),
176
- createdOn: getDisplayValue(record, 'sys_created_on'),
177
- updatedOn: getDisplayValue(record, 'sys_updated_on'),
178
- }));
179
-
180
- // Fetch elements for each section
181
- const sectionElements = new Map();
182
- for (const section of sections) {
183
- const elemParams = new URLSearchParams();
184
- elemParams.set('sysparm_limit', '100');
185
- elemParams.set('sysparm_fields', 'sys_id,sys_ui_section,element,label,type,position,row,col,mandatory,read_only,visible');
186
- elemParams.set('sysparm_display_value', 'all');
187
- elemParams.set('sysparm_query', `sys_ui_section=${section.sys_id}^ORDERBYposition`);
188
-
189
- try {
190
- const elemRecords = await app.sdk.list('sys_ui_element', elemParams);
191
- const elements = elemRecords.map(record => ({
192
- sys_id: getStringField(record, 'sys_id'),
193
- section: getStringField(record, 'sys_ui_section'),
194
- name: getDisplayValue(record, 'element'),
195
- label: getDisplayValue(record, 'label'),
196
- elementType: getDisplayValue(record, 'type'),
197
- position: getIntValue(record, 'position'),
198
- row: getIntValue(record, 'row'),
199
- column: getIntValue(record, 'col'),
200
- mandatory: getBoolValue(record, 'mandatory'),
201
- readOnly: getBoolValue(record, 'read_only'),
202
- visible: getBoolValue(record, 'visible'),
203
- }));
204
- sectionElements.set(section.sys_id, elements);
205
- } catch {
206
- sectionElements.set(section.sys_id, []);
207
- }
208
- }
209
-
210
- // Build formatted output
211
- const lines = [];
212
- lines.push('');
213
- lines.push(chalk.bold(chalk.hex('#e8a217')(`${table} (${viewName})`)));
214
- lines.push('');
215
-
216
- for (let i = 0; i < sections.length; i++) {
217
- const section = sections[i];
218
- const elements = sectionElements.get(section.sys_id) || [];
219
-
220
- let sectionTitle = section.caption;
221
- if (!sectionTitle && section.header && section.header !== 'false') {
222
- sectionTitle = section.header;
223
- }
224
- if (!sectionTitle) {
225
- sectionTitle = `Section ${i + 1}`;
226
- }
227
-
228
- lines.push(chalk.bold(chalk.hex('#666666')(`─ ${sectionTitle} ─`)));
229
-
230
- if (elements.length === 0) {
231
- lines.push(chalk.hex('#888888')(' (no fields)'));
232
- lines.push('');
233
- continue;
234
- }
235
-
236
- elements.sort((a, b) => a.position - b.position);
237
-
238
- for (const elem of elements) {
239
- if (elem.elementType && elem.elementType !== 'field') {
240
- continue;
241
- }
242
-
243
- let displayName = elem.name;
244
- if (!displayName) displayName = elem.label;
245
- if (!displayName) displayName = elem.elementType;
246
-
247
- const indicators = [];
248
- if (elem.mandatory) indicators.push('*');
249
- if (elem.readOnly) indicators.push('(RO)');
250
- const indicatorStr = indicators.length > 0 ? ` ${indicators.join(' ')}` : '';
251
-
252
- lines.push(` ${chalk.hex('#cccccc')(displayName)}${indicatorStr ? chalk.hex('#888888')(indicatorStr) : ''}`);
253
- }
254
-
255
- lines.push('');
256
- }
257
-
258
- lines.push('─────');
259
- lines.push('');
260
- lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
261
- lines.push(` ${`jsn forms list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
262
- lines.push(` ${`jsn columns --table ${table}`.padEnd(50)} ${chalk.hex('#888888')('View table columns')}`);
263
- lines.push('');
264
-
265
- const formatted = lines.join('\n');
266
-
267
- // Build structured data for JSON/quiet mode
268
- const sectionsData = sections.map(section => {
269
- const elements = (sectionElements.get(section.sys_id) || [])
270
- .filter(elem => !elem.elementType || elem.elementType === 'field')
271
- .sort((a, b) => a.position - b.position)
272
- .map(elem => ({
273
- sys_id: elem.sys_id,
274
- name: elem.name,
275
- label: elem.label,
276
- type: elem.elementType,
277
- position: elem.position,
278
- mandatory: elem.mandatory,
279
- read_only: elem.readOnly,
280
- }));
281
-
282
- let sectionTitle = section.caption;
283
- if (!sectionTitle && section.header && section.header !== 'false') {
284
- sectionTitle = section.header;
285
- }
286
- if (!sectionTitle) {
287
- sectionTitle = 'Section';
288
- }
289
-
290
- return {
291
- sys_id: section.sys_id,
292
- caption: sectionTitle,
293
- order: section.order,
294
- elements,
295
- };
296
- });
297
-
298
- app.ok({
299
- table,
300
- view: viewName,
301
- sections: sectionsData,
302
- _formatted: formatted,
303
- _context: {
304
- instance_url: app.getEffectiveInstance(),
305
- table: 'sys_ui_section',
306
- },
307
- }, {
308
- summary: `Form: ${table} (${viewName}) - ${sections.length} sections`,
309
- breadcrumbs: [
310
- { action: 'list', cmd: `jsn forms list ${table}`, description: 'List all views' },
311
- { action: 'columns', cmd: `jsn columns --table ${table}`, description: 'View table columns' },
312
- ],
313
- });
181
+ await showForm(app, argv.table, argv.view);
314
182
  }),
315
183
  });
316
184
  },
317
185
  handler: () => {
318
- console.log('Manage form layouts from the sys_ui_form_section table.');
186
+ console.log('Manage UI form layouts.');
319
187
  console.log('');
320
188
  console.log('Available subcommands:');
321
- console.log(' list <table> List form views for a table');
322
- console.log(' show <table> Show form layout for a table');
189
+ console.log(' list [table] List form views (shows table — view name)');
190
+ console.log(' show <table> Show form layout for a table');
323
191
  console.log('');
324
192
  console.log('Run "jsn forms <command> --help" for details.');
325
193
  },
@@ -1,5 +1,13 @@
1
1
  import { formatRecordForDisplay, interactiveList, getStringField } from '../../helpers.js';
2
2
 
3
+ function logIcon(level) {
4
+ level = (level || '').toLowerCase();
5
+ if (level === 'error') return '❌';
6
+ if (level === 'warning') return '⚠️';
7
+ if (level === 'information' || level === 'info') return 'ℹ️';
8
+ return '📝';
9
+ }
10
+
3
11
  export function logsCmd(wrap) {
4
12
  return {
5
13
  command: 'logs [subcommand]',
@@ -12,23 +20,47 @@ export function logsCmd(wrap) {
12
20
  aliases: ['ls'],
13
21
  describe: 'List system logs',
14
22
  builder: (y) => y
15
- .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
16
- .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
23
+ .option('query', { type: 'string', describe: 'Encoded query (e.g. "level=error" or "sourceLIKEscript")' })
24
+ .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns' })
17
25
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
26
  handler: wrap(async (argv, app) => {
19
27
  app.requireInstance();
20
- const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'created'];
28
+ const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'sys_created_on', 'sys_id', 'context_map'];
21
29
  const query = argv.query || '';
22
30
 
23
- // Try interactive picker first
24
31
  const picked = await interactiveList({
25
32
  app, table: 'syslog', singular: 'log entry', columns, limit: argv.limit, query, labelField: 'message',
26
- formatLabel: r => `[${getStringField(r, 'level') || '?'}] ${(getStringField(r, 'message') || '').substring(0, 80)}`,
33
+ formatLabel: r => {
34
+ const level = getStringField(r, 'level') || '';
35
+ const msg = (getStringField(r, 'message') || '').substring(0, 80);
36
+ const src = getStringField(r, 'source') || '';
37
+ return `${logIcon(level)} ${msg}${src ? ` [${src}]` : ''}`;
38
+ },
27
39
  });
28
- if (picked === undefined) return; // user cancelled
40
+ if (picked === undefined) return;
29
41
  if (picked) {
30
- picked._context = { instance_url: app.getEffectiveInstance(), table: 'syslog' };
31
- return app.ok(picked, { summary: `Log entry` });
42
+ const sysID = getStringField(picked, 'sys_id') || '';
43
+ // Fetch full record for rich display (like jsn logs show)
44
+ const full = await app.sdk.get('syslog', sysID) || picked;
45
+ full._context = { instance_url: app.getEffectiveInstance(), table: 'syslog' };
46
+ // Parse and pretty-print context_map JSON (SN API may return as string or object)
47
+ const cm = full.context_map;
48
+ if (cm) {
49
+ try {
50
+ const parsed = typeof cm === 'string' ? JSON.parse(cm) : cm;
51
+ full.context_map = Object.entries(parsed).map(([k, v]) => `${k}: ${v}`).join('\n');
52
+ } catch { /* leave as-is */ }
53
+ }
54
+ const level = getStringField(full, 'level') || '?';
55
+ const instance = app.getEffectiveInstance();
56
+ return app.ok(full, {
57
+ summary: `${logIcon(level)} ${level}: ${(getStringField(full, 'message') || '').substring(0, 80)}`,
58
+ breadcrumbs: [
59
+ { action: 'list', cmd: 'jsn logs list', description: 'Back to all logs' },
60
+ { action: 'view', cmd: `jsn logs show ${sysID}`, description: 'View full details' },
61
+ ...(instance && sysID ? [{ action: 'open', label: `${instance}/syslog_list.do?sysparm_query=sys_id=${sysID}`, description: 'Open in ServiceNow' }] : []),
62
+ ],
63
+ });
32
64
  }
33
65
 
34
66
  // Fall back to text/table
@@ -57,7 +89,8 @@ export function logsCmd(wrap) {
57
89
  if (!record) {
58
90
  throw new Error(`Log entry not found: ${argv.sys_id}`);
59
91
  }
60
- app.ok(record, { summary: `Log entry ${argv.sys_id}` });
92
+ const level = getStringField(record, 'level') || '?';
93
+ app.ok(record, { summary: `${logIcon(level)} ${level}: ${(getStringField(record, 'message') || '').substring(0, 80)}` });
61
94
  }),
62
95
  });
63
96
  },
@@ -148,6 +148,15 @@ export function groupMembersCmd(wrap) {
148
148
  }),
149
149
  });
150
150
  },
151
- handler: () => {},
151
+ handler: () => {
152
+ console.log('Manage group memberships in sys_user_grmember.');
153
+ console.log('');
154
+ console.log('Available subcommands:');
155
+ console.log(' list List group members');
156
+ console.log(' add Add a user to a group');
157
+ console.log(' remove Remove a user from a group');
158
+ console.log('');
159
+ console.log('Run "jsn groupmembers <command> --help" for details.');
160
+ },
152
161
  };
153
162
  }
@@ -148,6 +148,15 @@ export function groupRolesCmd(wrap) {
148
148
  }),
149
149
  });
150
150
  },
151
- handler: () => {},
151
+ handler: () => {
152
+ console.log('Manage group roles in sys_user_has_role.');
153
+ console.log('');
154
+ console.log('Available subcommands:');
155
+ console.log(' list List group roles');
156
+ console.log(' add Add a role to a group');
157
+ console.log(' remove Remove a role from a group');
158
+ console.log('');
159
+ console.log('Run "jsn grouproles <command> --help" for details.');
160
+ },
152
161
  };
153
162
  }
@@ -137,6 +137,17 @@ export function groupsCmd(wrap) {
137
137
  }),
138
138
  });
139
139
  },
140
- handler: () => {},
140
+ handler: () => {
141
+ console.log('Manage ServiceNow groups from the sys_user_group table.');
142
+ console.log('');
143
+ console.log('Available subcommands:');
144
+ console.log(' list List groups');
145
+ console.log(' show <name or sys_id> Show a group');
146
+ console.log(' create Create a new group');
147
+ console.log(' update <identifier> Update a group');
148
+ console.log(' delete <identifier> Delete a group');
149
+ console.log('');
150
+ console.log('Run "jsn groups <command> --help" for details.');
151
+ },
141
152
  };
142
153
  }
@@ -205,6 +205,18 @@ export function recordsCmd(wrap) {
205
205
  })
206
206
 
207
207
  },
208
- handler: () => {},
208
+ handler: () => {
209
+ console.log('Query and manage records in any ServiceNow table.');
210
+ console.log('');
211
+ console.log('Available subcommands:');
212
+ console.log(' list List records from a table');
213
+ console.log(' get Get a single record by sys_id');
214
+ console.log(' create Create a record');
215
+ console.log(' update Update a record');
216
+ console.log(' delete Delete a record');
217
+ console.log(' inspect Inspect table schema and statistics');
218
+ console.log('');
219
+ console.log('Run "jsn records <command> --help" for details.');
220
+ },
209
221
  };
210
222
  }
@@ -230,6 +230,16 @@ export function skillCmd(wrap) {
230
230
  }),
231
231
  });
232
232
  },
233
- handler: () => {},
233
+ handler: () => {
234
+ console.log('Manage the jsn AI agent skill file.');
235
+ console.log('');
236
+ console.log('Available subcommands:');
237
+ console.log(' show Show the bundled skill file');
238
+ console.log(' fetch Download the latest skill from GitHub');
239
+ console.log(' path Show skill file locations');
240
+ console.log(' install Install skill file to agents directory');
241
+ console.log('');
242
+ console.log('Run "jsn skill <command> --help" for details.');
243
+ },
234
244
  };
235
245
  }