@jacebenson/jsn 3.0.6 → 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.
@@ -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,57 +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
- const table = argv.table;
64
- const limit = argv.limit || 50;
131
+ app.requireInstance();
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
+ }
65
151
 
152
+ // Fallback
66
153
  const params = new URLSearchParams();
67
- params.set('sysparm_limit', String(limit));
68
- params.set('sysparm_fields', 'view');
69
- params.set('sysparm_group_by', 'view');
154
+ params.set('sysparm_limit', String(argv.limit));
155
+ params.set('sysparm_fields', 'name,view,sys_id');
70
156
  params.set('sysparm_display_value', 'all');
71
- const sysparmQuery = table ? `name=${table}^ORDERBYview` : 'ORDERBYview';
72
- params.set('sysparm_query', sysparmQuery);
73
-
74
- const records = await app.sdk.list('sys_ui_section', params);
75
-
76
- const viewMap = new Map();
77
- for (const record of records) {
78
- const view = getDisplayValue(record, 'view');
79
- if (view && !viewMap.has(view)) {
80
- viewMap.set(view, true);
81
- }
82
- }
83
-
84
- const views = Array.from(viewMap.keys()).sort();
85
- const defaultViews = views.filter(v => v === 'Default view');
86
- const workspaceViews = views.filter(v => v.toLowerCase().includes('workspace'));
87
- const otherViews = views.filter(v => !defaultViews.includes(v) && !workspaceViews.includes(v));
88
-
89
- 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);
90
159
 
160
+ const desc = table ? `for ${table}` : 'across all tables';
91
161
  app.ok({
92
- table,
93
- count: views.length,
94
- default: defaultViews,
95
- workspaces: workspaceViews,
96
- other: otherViews,
97
- records: recordsOut,
98
- 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() },
99
167
  }, {
100
- summary: `${views.length} views for ${table}`,
101
- breadcrumbs: [
102
- { action: 'show', cmd: `jsn dev forms show ${table} --view "Default view"`, description: 'Show Default view layout' },
103
- ],
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' }] } : {}),
104
170
  });
105
171
  }),
106
172
  })
@@ -111,207 +177,19 @@ export function formsCmd(wrap) {
111
177
  builder: (y) => y
112
178
  .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
113
179
  handler: wrap(async (argv, app) => {
114
- const table = argv.table;
115
- const viewName = argv.view;
116
-
117
- // Look up view sys_id
118
- let viewSysID = '';
119
- const viewParams = new URLSearchParams();
120
- viewParams.set('sysparm_limit', '1');
121
- viewParams.set('sysparm_fields', 'sys_id');
122
- viewParams.set('sysparm_query', `name=${viewName}`);
123
- try {
124
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
125
- if (viewRecords.length > 0) {
126
- viewSysID = getStringField(viewRecords[0], 'sys_id');
127
- }
128
- } catch {
129
- // ignore
130
- }
131
-
132
- if (!viewSysID) {
133
- viewParams.set('sysparm_query', `title=${viewName}`);
134
- try {
135
- const viewRecords = await app.sdk.list('sys_ui_view', viewParams);
136
- if (viewRecords.length > 0) {
137
- viewSysID = getStringField(viewRecords[0], 'sys_id');
138
- }
139
- } catch {
140
- // ignore
141
- }
142
- }
143
-
144
- if (!viewSysID) {
145
- viewSysID = viewName;
146
- }
147
-
148
- // Fetch sections
149
- const params = new URLSearchParams();
150
- params.set('sysparm_limit', '100');
151
- params.set('sysparm_fields', 'sys_id,name,view,caption,header,order,active,sys_created_on,sys_updated_on');
152
- params.set('sysparm_display_value', 'all');
153
-
154
- const parts = [];
155
- if (table) parts.push(`name=${table}`);
156
- if (viewSysID) parts.push(`view=${viewSysID}`);
157
- const sysparmQuery = parts.length > 0 ? `${parts.join('^')}^ORDERBYorder` : 'ORDERBYorder';
158
- params.set('sysparm_query', sysparmQuery);
159
-
160
- const records = await app.sdk.list('sys_ui_section', params);
161
- if (records.length === 0) {
162
- throw new Error(`no form sections found for ${table} with view "${viewName}"`);
163
- }
164
-
165
- // Parse sections
166
- const sections = records.map(record => ({
167
- sys_id: getStringField(record, 'sys_id'),
168
- name: getDisplayValue(record, 'name'),
169
- view: getDisplayValue(record, 'view'),
170
- caption: getDisplayValue(record, 'caption'),
171
- header: getDisplayValue(record, 'header'),
172
- order: getIntValue(record, 'order'),
173
- active: getBoolValue(record, 'active'),
174
- createdOn: getDisplayValue(record, 'sys_created_on'),
175
- updatedOn: getDisplayValue(record, 'sys_updated_on'),
176
- }));
177
-
178
- // Fetch elements for each section
179
- const sectionElements = new Map();
180
- for (const section of sections) {
181
- const elemParams = new URLSearchParams();
182
- elemParams.set('sysparm_limit', '100');
183
- elemParams.set('sysparm_fields', 'sys_id,sys_ui_section,element,label,type,position,row,col,mandatory,read_only,visible');
184
- elemParams.set('sysparm_display_value', 'all');
185
- elemParams.set('sysparm_query', `sys_ui_section=${section.sys_id}^ORDERBYposition`);
186
-
187
- try {
188
- const elemRecords = await app.sdk.list('sys_ui_element', elemParams);
189
- const elements = elemRecords.map(record => ({
190
- sys_id: getStringField(record, 'sys_id'),
191
- section: getStringField(record, 'sys_ui_section'),
192
- name: getDisplayValue(record, 'element'),
193
- label: getDisplayValue(record, 'label'),
194
- elementType: getDisplayValue(record, 'type'),
195
- position: getIntValue(record, 'position'),
196
- row: getIntValue(record, 'row'),
197
- column: getIntValue(record, 'col'),
198
- mandatory: getBoolValue(record, 'mandatory'),
199
- readOnly: getBoolValue(record, 'read_only'),
200
- visible: getBoolValue(record, 'visible'),
201
- }));
202
- sectionElements.set(section.sys_id, elements);
203
- } catch {
204
- sectionElements.set(section.sys_id, []);
205
- }
206
- }
207
-
208
- // Build formatted output
209
- const lines = [];
210
- lines.push('');
211
- lines.push(chalk.bold(chalk.hex('#e8a217')(`${table} (${viewName})`)));
212
- lines.push('');
213
-
214
- for (let i = 0; i < sections.length; i++) {
215
- const section = sections[i];
216
- const elements = sectionElements.get(section.sys_id) || [];
217
-
218
- let sectionTitle = section.caption;
219
- if (!sectionTitle && section.header && section.header !== 'false') {
220
- sectionTitle = section.header;
221
- }
222
- if (!sectionTitle) {
223
- sectionTitle = `Section ${i + 1}`;
224
- }
225
-
226
- lines.push(chalk.bold(chalk.hex('#666666')(`─ ${sectionTitle} ─`)));
227
-
228
- if (elements.length === 0) {
229
- lines.push(chalk.hex('#888888')(' (no fields)'));
230
- lines.push('');
231
- continue;
232
- }
233
-
234
- elements.sort((a, b) => a.position - b.position);
235
-
236
- for (const elem of elements) {
237
- if (elem.elementType && elem.elementType !== 'field') {
238
- continue;
239
- }
240
-
241
- let displayName = elem.name;
242
- if (!displayName) displayName = elem.label;
243
- if (!displayName) displayName = elem.elementType;
244
-
245
- const indicators = [];
246
- if (elem.mandatory) indicators.push('*');
247
- if (elem.readOnly) indicators.push('(RO)');
248
- const indicatorStr = indicators.length > 0 ? ` ${indicators.join(' ')}` : '';
249
-
250
- lines.push(` ${chalk.hex('#cccccc')(displayName)}${indicatorStr ? chalk.hex('#888888')(indicatorStr) : ''}`);
251
- }
252
-
253
- lines.push('');
254
- }
255
-
256
- lines.push('─────');
257
- lines.push('');
258
- lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
259
- lines.push(` ${`jsn dev forms list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
260
- lines.push(` ${`jsn dev columns --table ${table}`.padEnd(50)} ${chalk.hex('#888888')('View table columns')}`);
261
- lines.push('');
262
-
263
- const formatted = lines.join('\n');
264
-
265
- // Build structured data for JSON/quiet mode
266
- const sectionsData = sections.map(section => {
267
- const elements = (sectionElements.get(section.sys_id) || [])
268
- .filter(elem => !elem.elementType || elem.elementType === 'field')
269
- .sort((a, b) => a.position - b.position)
270
- .map(elem => ({
271
- sys_id: elem.sys_id,
272
- name: elem.name,
273
- label: elem.label,
274
- type: elem.elementType,
275
- position: elem.position,
276
- mandatory: elem.mandatory,
277
- read_only: elem.readOnly,
278
- }));
279
-
280
- let sectionTitle = section.caption;
281
- if (!sectionTitle && section.header && section.header !== 'false') {
282
- sectionTitle = section.header;
283
- }
284
- if (!sectionTitle) {
285
- sectionTitle = 'Section';
286
- }
287
-
288
- return {
289
- sys_id: section.sys_id,
290
- caption: sectionTitle,
291
- order: section.order,
292
- elements,
293
- };
294
- });
295
-
296
- app.ok({
297
- table,
298
- view: viewName,
299
- sections: sectionsData,
300
- _formatted: formatted,
301
- _context: {
302
- instance_url: app.getEffectiveInstance(),
303
- table: 'sys_ui_section',
304
- },
305
- }, {
306
- summary: `Form: ${table} (${viewName}) - ${sections.length} sections`,
307
- breadcrumbs: [
308
- { action: 'list', cmd: `jsn dev forms list ${table}`, description: 'List all views' },
309
- { action: 'columns', cmd: `jsn dev columns --table ${table}`, description: 'View table columns' },
310
- ],
311
- });
180
+ app.requireInstance();
181
+ await showForm(app, argv.table, argv.view);
312
182
  }),
313
183
  });
314
184
  },
315
- handler: () => {},
185
+ handler: () => {
186
+ console.log('Manage UI form layouts.');
187
+ console.log('');
188
+ console.log('Available subcommands:');
189
+ console.log(' list [table] List form views (shows table — view name)');
190
+ console.log(' show <table> Show form layout for a table');
191
+ console.log('');
192
+ console.log('Run "jsn forms <command> --help" for details.');
193
+ },
316
194
  };
317
195
  }
@@ -51,6 +51,7 @@ export function listsCmd(wrap) {
51
51
  })
52
52
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
53
53
  handler: wrap(async (argv, app) => {
54
+ app.requireInstance();
54
55
  const table = argv.table;
55
56
  const limit = argv.limit || 50;
56
57
 
@@ -90,7 +91,7 @@ export function listsCmd(wrap) {
90
91
  }, {
91
92
  summary: `${views.length} list views for ${table}`,
92
93
  breadcrumbs: [
93
- { action: 'show', cmd: `jsn dev lists show ${table} --view "Default view"`, description: 'Show Default view columns' },
94
+ { action: 'show', cmd: `jsn lists show ${table} --view "Default view"`, description: 'Show Default view columns' },
94
95
  ],
95
96
  });
96
97
  }),
@@ -102,6 +103,7 @@ export function listsCmd(wrap) {
102
103
  builder: (y) => y
103
104
  .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
104
105
  handler: wrap(async (argv, app) => {
106
+ app.requireInstance();
105
107
  const table = argv.table;
106
108
  const viewName = argv.view;
107
109
 
@@ -195,8 +197,8 @@ export function listsCmd(wrap) {
195
197
  lines.push('─────');
196
198
  lines.push('');
197
199
  lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
198
- lines.push(` ${`jsn dev lists list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
199
- lines.push(` ${`jsn dev forms show ${table} --view "${viewName}"`.padEnd(50)} ${chalk.hex('#888888')('Show form layout')}`);
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')}`);
200
202
  lines.push('');
201
203
 
202
204
  const formatted = lines.join('\n');
@@ -225,13 +227,21 @@ export function listsCmd(wrap) {
225
227
  }, {
226
228
  summary: `List: ${table} (${viewName}) - ${elements.length} columns`,
227
229
  breadcrumbs: [
228
- { action: 'list', cmd: `jsn dev lists list ${table}`, description: 'List all views' },
229
- { action: 'form', cmd: `jsn dev forms show ${table} --view "${viewName}"`, description: 'Show form layout' },
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' },
230
232
  ],
231
233
  });
232
234
  }),
233
235
  });
234
236
  },
235
- handler: () => {},
237
+ 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');
243
+ console.log('');
244
+ console.log('Run "jsn lists <command> --help" for details.');
245
+ },
236
246
  };
237
247
  }
@@ -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,21 +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
- const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'created'];
27
+ app.requireInstance();
28
+ const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'sys_created_on', 'sys_id', 'context_map'];
20
29
  const query = argv.query || '';
21
30
 
22
- // Try interactive picker first
23
31
  const picked = await interactiveList({
24
32
  app, table: 'syslog', singular: 'log entry', columns, limit: argv.limit, query, labelField: 'message',
25
- 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
+ },
26
39
  });
40
+ if (picked === undefined) return;
27
41
  if (picked) {
28
- picked._context = { instance_url: app.getEffectiveInstance(), table: 'syslog' };
29
- 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
+ });
30
64
  }
31
65
 
32
66
  // Fall back to text/table
@@ -55,10 +89,19 @@ export function logsCmd(wrap) {
55
89
  if (!record) {
56
90
  throw new Error(`Log entry not found: ${argv.sys_id}`);
57
91
  }
58
- 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)}` });
59
94
  }),
60
95
  });
61
96
  },
62
- handler: () => {},
97
+ handler: () => {
98
+ console.log('View system logs from the syslog table.');
99
+ console.log('');
100
+ console.log('Available subcommands:');
101
+ console.log(' list List log entries');
102
+ console.log(' show <sys_id> Show a log entry by sys_id');
103
+ console.log('');
104
+ console.log('Run "jsn logs <command> --help" for details.');
105
+ },
63
106
  };
64
107
  }
@@ -14,7 +14,7 @@ export function scopesCmd(wrap) {
14
14
  builder: (y) => y
15
15
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
16
16
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
17
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
17
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
18
  handler: wrap(async (argv, app) => {
19
19
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'scope', 'short_description', 'active'];
20
20
  const query = argv.query || '';
@@ -23,6 +23,7 @@ export function scopesCmd(wrap) {
23
23
  app, table: 'sys_scope', singular: 'scope', columns, limit: argv.limit, query, labelField: 'name',
24
24
  formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'scope') || '?'}]`,
25
25
  });
26
+ if (picked === undefined) return; // user cancelled
26
27
  if (picked) {
27
28
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_scope' };
28
29
  return app.ok(picked, { summary: `Scope: ${getStringField(picked, 'name')}` });
@@ -143,7 +144,7 @@ export function scopesCmd(wrap) {
143
144
  summary: `Created scope: ${scope}`,
144
145
  breadcrumbs: [{
145
146
  action: 'show',
146
- cmd: `jsn dev scopes show ${scope}`,
147
+ cmd: `jsn scopes show ${scope}`,
147
148
  description: 'View the new scope',
148
149
  }],
149
150
  });
@@ -159,7 +160,7 @@ export function scopesCmd(wrap) {
159
160
  console.log(' show <scope> Show a scope');
160
161
  console.log(' set <scope> Set the current application scope');
161
162
  console.log(' create Create a new application scope');
162
- console.log('\nRun "jsn dev scopes <command> --help" for details.');
163
+ console.log('\nRun "jsn scopes <command> --help" for details.');
163
164
  }
164
165
  },
165
166
  };