@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.
- package/package.json +3 -2
- package/skills/servicenow/SKILL.md +41 -123
- package/src/cli.js +247 -200
- package/src/commands/_ticket.js +14 -1
- package/src/commands/auth.js +10 -1
- package/src/commands/catalog.js +179 -292
- package/src/commands/dev/_generic.js +39 -51
- package/src/commands/dev/_simple.js +27 -0
- package/src/commands/dev/b4rules.js +5 -0
- package/src/commands/dev/forms.js +123 -255
- package/src/commands/dev/logs.js +42 -9
- package/src/commands/groupmembers.js +10 -1
- package/src/commands/grouproles.js +10 -1
- package/src/commands/groups.js +12 -1
- package/src/commands/records.js +13 -1
- package/src/commands/skill.js +11 -1
- package/src/commands/tickets.js +12 -1
- package/src/commands/users.js +12 -1
- package/src/help.js +125 -78
- package/src/helpers.js +33 -20
- package/src/paginated-search.js +187 -155
- package/src/sdk.js +4 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
124
|
+
command: 'list [table]',
|
|
54
125
|
aliases: ['ls'],
|
|
55
|
-
describe: 'List form views
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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: `${
|
|
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
|
-
|
|
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
|
|
186
|
+
console.log('Manage UI form layouts.');
|
|
319
187
|
console.log('');
|
|
320
188
|
console.log('Available subcommands:');
|
|
321
|
-
console.log(' list
|
|
322
|
-
console.log(' show <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
|
},
|
package/src/commands/dev/logs.js
CHANGED
|
@@ -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. "
|
|
16
|
-
.option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns
|
|
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', '
|
|
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 =>
|
|
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;
|
|
40
|
+
if (picked === undefined) return;
|
|
29
41
|
if (picked) {
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/groups.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/records.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/skill.js
CHANGED
|
@@ -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
|
}
|