@jacebenson/jsn 4.0.4 → 4.2.0
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/README.md +18 -2
- package/package.json +4 -3
- package/src/app.js +48 -64
- package/src/auth.js +46 -11
- package/src/capabilities.js +128 -0
- package/src/cli.js +129 -65
- package/src/commands/_ticket.js +37 -93
- package/src/commands/approvals.js +350 -0
- package/src/commands/atf.js +635 -0
- package/src/commands/auth.js +93 -13
- package/src/commands/catalog/enrich.js +139 -0
- package/src/commands/catalog.js +25 -81
- package/src/commands/cmdb.js +613 -0
- package/src/commands/codesearch.js +103 -0
- package/src/commands/dev/_generic.js +63 -111
- package/src/commands/dev/_simple.js +16 -12
- package/src/commands/dev/b4rules.js +1 -1
- package/src/commands/dev/domains.js +4 -0
- package/src/commands/dev/eval.js +65 -7
- package/src/commands/dev/flows.js +3 -0
- package/src/commands/dev/logs.js +97 -0
- package/src/commands/dev/rest.js +6 -0
- package/src/commands/dev/restmessage.js +1 -0
- package/src/commands/dev/scopes.js +49 -25
- package/src/commands/dev/scrapi.js +5 -0
- package/src/commands/dev/soapmessages.js +1 -0
- package/src/commands/dev/updatesets.js +145 -15
- package/src/commands/dev.js +3 -2
- package/src/commands/docs/db.js +14 -0
- package/src/commands/docs/docs.js +9 -21
- package/src/commands/docs/search.js +33 -10
- package/src/commands/groups.js +3 -0
- package/src/commands/records.js +274 -12
- package/src/commands/setup.js +5 -0
- package/src/commands/skill.js +3 -0
- package/src/commands/snippets.js +155 -0
- package/src/commands/tickets.js +9 -29
- package/src/commands/users.js +9 -35
- package/src/commands/version.js +3 -0
- package/src/errors.js +107 -1
- package/src/help.js +1 -0
- package/src/helpers.js +131 -15
- package/src/mutations.js +36 -91
- package/src/output.js +195 -64
- package/src/resolve-record.js +71 -0
- package/src/sdk.js +108 -8
- package/src/session.js +214 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// jsn codesearch — search across ServiceNow code artifacts (script includes,
|
|
2
|
+
// business rules, client scripts, UI actions, ...) via the sn_codesearch
|
|
3
|
+
// plugin REST API. Issue #180.
|
|
4
|
+
|
|
5
|
+
import { errUsage } from '../errors.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_SEARCH_GROUP = 'sn_devstudio.Studio Search Group';
|
|
8
|
+
|
|
9
|
+
// Flatten the API's nested shape into one row per record/field so the output
|
|
10
|
+
// controller can render it as a table / CSV / JSON.
|
|
11
|
+
//
|
|
12
|
+
// API shape:
|
|
13
|
+
// result: [ { recordType, hits: [ { name, className, tableLabel,
|
|
14
|
+
// matches: [ { field, fieldLabel,
|
|
15
|
+
// lineMatches: [ { line, context, escaped } ] } ] } ] } ] }
|
|
16
|
+
// NOTE: with ?table=<t> the API returns a single object, not an array.
|
|
17
|
+
export function flattenHits(result) {
|
|
18
|
+
const groups = Array.isArray(result) ? result : (result ? [result] : []);
|
|
19
|
+
const rows = [];
|
|
20
|
+
for (const group of groups) {
|
|
21
|
+
for (const hit of group.hits || []) {
|
|
22
|
+
for (const match of hit.matches || []) {
|
|
23
|
+
const lines = (match.lineMatches || []).filter((lm) => lm.context);
|
|
24
|
+
rows.push({
|
|
25
|
+
table: hit.className || group.recordType,
|
|
26
|
+
name: hit.name,
|
|
27
|
+
field: match.field,
|
|
28
|
+
lines: lines.map((lm) => lm.line).join(','),
|
|
29
|
+
matches: lines.length,
|
|
30
|
+
context: lines[0]?.context?.trim() || '',
|
|
31
|
+
sys_id: hit.sys_id || hit.sysId || '',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pre-rendered text view for styled/terminal output (output.js uses
|
|
40
|
+
// data._formatted when present).
|
|
41
|
+
function renderFormatted(rows, term) {
|
|
42
|
+
if (rows.length === 0) return `No code matches for "${term}".\n`;
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
out.push(`${r.table} ${r.name} .${r.field} (lines ${r.lines})`);
|
|
46
|
+
if (r.context) out.push(` ${r.context}`);
|
|
47
|
+
}
|
|
48
|
+
return out.join('\n') + '\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function codesearchCmd(wrap) {
|
|
52
|
+
return {
|
|
53
|
+
command: 'codesearch [subcommand]',
|
|
54
|
+
aliases: ['codesearch'],
|
|
55
|
+
describe: 'Search code across ServiceNow artifacts (script includes, business rules, client scripts, ...)',
|
|
56
|
+
builder: (yargs) => yargs
|
|
57
|
+
.command({
|
|
58
|
+
command: 'search <term>',
|
|
59
|
+
describe: 'Search code for a term (e.g. "codesearch search GlideAjax")',
|
|
60
|
+
builder: (y) => y
|
|
61
|
+
.positional('term', { type: 'string', describe: 'Search term' })
|
|
62
|
+
.option('limit', { alias: 'l', type: 'number', default: 100, describe: 'Max results' })
|
|
63
|
+
.option('table', { alias: 't', type: 'string', describe: 'Restrict to one source table (e.g. sys_script_include)' })
|
|
64
|
+
.option('scope', { alias: 's', type: 'string', describe: 'Restrict to an app scope (e.g. x_my_app)' })
|
|
65
|
+
.option('search-group', { type: 'string', default: DEFAULT_SEARCH_GROUP, describe: 'Search group to use' })
|
|
66
|
+
.option('all-scopes', { type: 'boolean', default: true, describe: 'Search across all scopes (default true)' }),
|
|
67
|
+
handler: wrap(async (argv, app) => {
|
|
68
|
+
if (!argv.term || !argv.term.trim()) {
|
|
69
|
+
throw errUsage('A search term is required', 'Example: jsn codesearch search GlideAjax');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
params.set('term', argv.term);
|
|
74
|
+
params.set('limit', String(argv.limit));
|
|
75
|
+
params.set('search_group', argv.searchGroup);
|
|
76
|
+
params.set('search_all_scopes', String(argv.allScopes));
|
|
77
|
+
if (argv.table) params.set('table', argv.table);
|
|
78
|
+
if (argv.scope) params.set('scope', argv.scope);
|
|
79
|
+
|
|
80
|
+
const url = `${app.getEffectiveInstance()}/api/sn_codesearch/code_search/search?${params.toString()}`;
|
|
81
|
+
let payload;
|
|
82
|
+
try {
|
|
83
|
+
payload = await app.sdk.request(url);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code === 'api_error' && /404/.test(err.message)) {
|
|
86
|
+
err.message += '\n\nHint: the sn_codesearch plugin may not be active on this instance.';
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = payload?.result || [];
|
|
92
|
+
const rows = flattenHits(result);
|
|
93
|
+
|
|
94
|
+
app.ok(
|
|
95
|
+
{ term: argv.term, count: rows.length, records: rows, _formatted: renderFormatted(rows, argv.term) },
|
|
96
|
+
{ summary: `${rows.length} code match(es) for "${argv.term}"` },
|
|
97
|
+
);
|
|
98
|
+
}),
|
|
99
|
+
})
|
|
100
|
+
.demandCommand(1, 'Specify a subcommand: search'),
|
|
101
|
+
handler: () => {},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Generic dev subcommand builder for table-based CRUD
|
|
2
2
|
|
|
3
|
-
import { formatRecordForDisplay, getStringField,
|
|
3
|
+
import { formatRecordForDisplay, getStringField, parseDataArg, confirmDelete, interactiveList, canPrompt } from '../../helpers.js';
|
|
4
|
+
import { resolveRecord, unwrapSysId } from '../../resolve-record.js';
|
|
4
5
|
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { FormatAuto } from '../../output.js';
|
|
7
|
+
import { declareCapabilities } from '../../capabilities.js';
|
|
7
8
|
|
|
8
9
|
function vowelArticle(word) {
|
|
9
10
|
const first = word.charAt(0).toLowerCase();
|
|
@@ -75,10 +76,31 @@ async function resolveRecordScope(sdk, record) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Scope guard for update/delete on scoped tables: when scopeValidation is
|
|
81
|
+
* enabled and the record lives outside the current scope, refuse with a
|
|
82
|
+
* "switch scope first" error. Pass the resolved record straight through.
|
|
83
|
+
*/
|
|
84
|
+
async function guardScope(sdk, record) {
|
|
85
|
+
const recordScope = await resolveRecordScope(sdk, record);
|
|
86
|
+
const scopeErr = await checkScope(sdk, recordScope);
|
|
87
|
+
if (scopeErr) {
|
|
88
|
+
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
|
|
89
|
+
}
|
|
90
|
+
return record;
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {}) {
|
|
79
94
|
const showFields = opts.showFields !== undefined ? opts.showFields : null;
|
|
80
95
|
const singular = toSingular(name, opts.singular);
|
|
81
96
|
const readOnly = opts.readOnly || false;
|
|
97
|
+
// Capability declaration: the readOnly flag drives the mutation registry.
|
|
98
|
+
// buildDevCmd commands are registered both at root and under `jsn dev`
|
|
99
|
+
// unless opts.devAlias says otherwise (root-only commands pass false).
|
|
100
|
+
declareCapabilities(name, {
|
|
101
|
+
mutationSubcommands: readOnly ? [] : ['create', 'update', 'delete'],
|
|
102
|
+
devAlias: opts.devAlias !== false,
|
|
103
|
+
});
|
|
82
104
|
const scopeValidation = opts.scopeValidation || false;
|
|
83
105
|
const extraQuery = opts.extraQuery || '';
|
|
84
106
|
const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
|
|
@@ -111,66 +133,32 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
111
133
|
const limit = argv.limit;
|
|
112
134
|
|
|
113
135
|
// Interactive picker with pagination
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
} catch { /* non-critical */ }
|
|
121
|
-
|
|
122
|
-
const source = async (term, offset, { signal: _signal } = {}) => {
|
|
123
|
-
const params = new URLSearchParams();
|
|
124
|
-
params.set('sysparm_limit', String(limit));
|
|
125
|
-
params.set('sysparm_display_value', 'all');
|
|
126
|
-
params.set('sysparm_fields', ['sys_id', 'name', 'sys_scope'].join(','));
|
|
127
|
-
if (term) {
|
|
128
|
-
params.set('sysparm_query', `nameLIKE${term}^ORDERBYDESCsys_updated_on`);
|
|
129
|
-
} else {
|
|
130
|
-
params.set('sysparm_query', (extraQuery ? extraQuery + '^' : '') + 'ORDERBYDESCsys_updated_on');
|
|
131
|
-
params.set('sysparm_offset', String(offset));
|
|
132
|
-
}
|
|
133
|
-
const records = await app.sdk.list(table, params);
|
|
134
|
-
return records.map(r => {
|
|
136
|
+
if (canPrompt() && app.output.getFormat() === FormatAuto && !query) {
|
|
137
|
+
const picked = await interactiveList({
|
|
138
|
+
app, table, singular, columns: ['name', 'sys_scope'], limit, query: extraQuery,
|
|
139
|
+
labelField: 'name',
|
|
140
|
+
message: `Select ${vowelArticle(singular)} ${singular}`,
|
|
141
|
+
formatLabel: (r) => {
|
|
135
142
|
const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
|
|
136
143
|
const scope = getStringField(r, 'sys_scope') || '';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return { name: label, value: recordName };
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const selectedName = await paginatedSearch({
|
|
144
|
-
message: `Select ${vowelArticle(singular)} ${singular}`,
|
|
145
|
-
pageSize: 10,
|
|
146
|
-
totalCount,
|
|
147
|
-
source,
|
|
144
|
+
return (scope && scope !== 'global') ? `${recordName} [${scope}]` : recordName;
|
|
145
|
+
},
|
|
148
146
|
});
|
|
147
|
+
if (picked === undefined || picked === null) return; // cancelled, or table is empty
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
showParams.set('sysparm_display_value', 'all');
|
|
156
|
-
showParams.set('sysparm_limit', '1');
|
|
157
|
-
const showRecords = await app.sdk.list(table, showParams);
|
|
158
|
-
if (showRecords.length === 0) {
|
|
159
|
-
throw new Error(`${singular} not found: ${recordName}`);
|
|
160
|
-
}
|
|
161
|
-
const record = showRecords[0];
|
|
162
|
-
record._context = { instance_url: app.getEffectiveInstance(), table };
|
|
163
|
-
if (opts.onShow) {
|
|
164
|
-
await opts.onShow(record, app);
|
|
165
|
-
}
|
|
166
|
-
app.ok(record, {
|
|
167
|
-
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
|
|
168
|
-
breadcrumbs: [
|
|
169
|
-
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} --data '{...}'`, description: `Update this ${singular}` }]),
|
|
170
|
-
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
171
|
-
],
|
|
172
|
-
});
|
|
149
|
+
const record = picked;
|
|
150
|
+
const recordName = getStringField(record, 'name') || getStringField(record, 'sys_id');
|
|
151
|
+
record._context = { instance_url: app.getEffectiveInstance(), table };
|
|
152
|
+
if (opts.onShow) {
|
|
153
|
+
await opts.onShow(record, app);
|
|
173
154
|
}
|
|
155
|
+
app.ok(record, {
|
|
156
|
+
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
|
|
157
|
+
breadcrumbs: [
|
|
158
|
+
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} --data '{...}'`, description: `Update this ${singular}` }]),
|
|
159
|
+
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
174
162
|
return;
|
|
175
163
|
}
|
|
176
164
|
|
|
@@ -201,34 +189,25 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
201
189
|
describe: `Show ${vowelArticle(singular)} ${singular} by name or sys_id`,
|
|
202
190
|
handler: wrap(async (argv, app) => {
|
|
203
191
|
const id = argv.identifier;
|
|
204
|
-
const queryField = isHexString(id) && id.length === 32 ? 'sys_id' : 'name';
|
|
205
|
-
assertSafeExactMatch(id);
|
|
206
|
-
const params = new URLSearchParams();
|
|
207
|
-
params.set('sysparm_query', `${queryField}=${id}`);
|
|
208
|
-
params.set('sysparm_limit', '1');
|
|
209
|
-
params.set('sysparm_display_value', 'all');
|
|
210
192
|
// Only restrict sysparm_fields if showFields is explicitly set.
|
|
211
193
|
// Go version fetches all fields for show unless explicitly restricted.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
throw new Error(`${singular} not found: ${id}`);
|
|
218
|
-
}
|
|
219
|
-
records[0]._context = {
|
|
194
|
+
const fields = (showFields && showFields.length > 0)
|
|
195
|
+
? [...new Set(['sys_id', ...showFields])]
|
|
196
|
+
: undefined;
|
|
197
|
+
const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular, fields });
|
|
198
|
+
record._context = {
|
|
220
199
|
instance_url: app.getEffectiveInstance(),
|
|
221
200
|
table,
|
|
222
201
|
};
|
|
223
202
|
|
|
224
203
|
if (opts.onShow) {
|
|
225
|
-
await opts.onShow(
|
|
204
|
+
await opts.onShow(record, app);
|
|
226
205
|
}
|
|
227
206
|
|
|
228
|
-
const summary = typeof showSummary === 'function' ? showSummary(
|
|
229
|
-
const breadcrumbs = typeof showBreadcrumbs === 'function' ? showBreadcrumbs(
|
|
207
|
+
const summary = typeof showSummary === 'function' ? showSummary(record, id) : showSummary;
|
|
208
|
+
const breadcrumbs = typeof showBreadcrumbs === 'function' ? showBreadcrumbs(record, id) : showBreadcrumbs;
|
|
230
209
|
|
|
231
|
-
app.ok(
|
|
210
|
+
app.ok(record, { summary, breadcrumbs });
|
|
232
211
|
}),
|
|
233
212
|
});
|
|
234
213
|
|
|
@@ -259,26 +238,12 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
259
238
|
.option('data-file', { type: 'string', describe: 'Read JSON payload from file' }),
|
|
260
239
|
handler: wrap(async (argv, app) => {
|
|
261
240
|
const id = argv.identifier;
|
|
262
|
-
const
|
|
263
|
-
assertSafeExactMatch(id);
|
|
264
|
-
const findParams = new URLSearchParams();
|
|
265
|
-
findParams.set('sysparm_query', `${queryField}=${id}`);
|
|
266
|
-
findParams.set('sysparm_limit', '1');
|
|
267
|
-
findParams.set('sysparm_display_value', 'all');
|
|
268
|
-
const records = await app.sdk.list(table, findParams);
|
|
269
|
-
if (records.length === 0) {
|
|
270
|
-
throw new Error(`${singular} not found: ${id}`);
|
|
271
|
-
}
|
|
272
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
273
|
-
const recordScope = await resolveRecordScope(app.sdk, records[0]);
|
|
274
|
-
|
|
241
|
+
const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular });
|
|
275
242
|
if (scopeValidation) {
|
|
276
|
-
|
|
277
|
-
if (scopeErr) {
|
|
278
|
-
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
|
|
279
|
-
}
|
|
243
|
+
await guardScope(app.sdk, record);
|
|
280
244
|
}
|
|
281
245
|
|
|
246
|
+
const sysID = unwrapSysId(record);
|
|
282
247
|
const recordData = parseDataArg(argv);
|
|
283
248
|
const updated = await app.sdk.update(table, sysID, recordData);
|
|
284
249
|
app.ok(updated, { summary: `Updated ${singular} ${id}` });
|
|
@@ -290,27 +255,14 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
290
255
|
builder: (y) => y.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
291
256
|
handler: wrap(async (argv, app) => {
|
|
292
257
|
const id = argv.identifier;
|
|
293
|
-
const
|
|
294
|
-
assertSafeExactMatch(id);
|
|
295
|
-
const findParams = new URLSearchParams();
|
|
296
|
-
findParams.set('sysparm_query', `${queryField}=${id}`);
|
|
297
|
-
findParams.set('sysparm_limit', '1');
|
|
298
|
-
findParams.set('sysparm_display_value', 'all');
|
|
299
|
-
const records = await app.sdk.list(table, findParams);
|
|
300
|
-
if (records.length === 0) {
|
|
301
|
-
throw new Error(`${singular} not found: ${id}`);
|
|
302
|
-
}
|
|
303
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
304
|
-
const recordName = getStringField(records[0], 'name') || id;
|
|
305
|
-
const recordScope = await resolveRecordScope(app.sdk, records[0]);
|
|
306
|
-
|
|
258
|
+
const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular });
|
|
307
259
|
if (scopeValidation) {
|
|
308
|
-
|
|
309
|
-
if (scopeErr) {
|
|
310
|
-
throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
|
|
311
|
-
}
|
|
260
|
+
await guardScope(app.sdk, record);
|
|
312
261
|
}
|
|
313
262
|
|
|
263
|
+
const sysID = unwrapSysId(record);
|
|
264
|
+
const recordName = getStringField(record, 'name') || id;
|
|
265
|
+
|
|
314
266
|
await confirmDelete(app, argv, `Delete ${singular} '${recordName}'`);
|
|
315
267
|
|
|
316
268
|
await app.sdk.delete(table, sysID);
|
|
@@ -56,7 +56,12 @@ export const clientScriptsCmd = (wrap) => buildDevCmd('clientscripts', 'sys_scri
|
|
|
56
56
|
|
|
57
57
|
export const uiActionsCmd = (wrap) => buildDevCmd('uiactions', 'sys_ui_action', ['uiaction', 'ua'], ['name', 'table', 'active', 'order', 'sys_scope'], wrap, { singular: 'UI action', scopeValidation: true });
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// NOTE: uipolicies has exactly ONE definition — the rich one in
|
|
60
|
+
// ./uipolicies.js (re-exported below at the bottom of this file). A plain
|
|
61
|
+
// buildDevCmd('uipolicies', ...) factory used to live here, but yargs keeps
|
|
62
|
+
// the LAST registration of a duplicate command name, and cli.js registers
|
|
63
|
+
// this module's uiPoliciesCmd before uipoliciesCmd — so the factory version
|
|
64
|
+
// was dead code shadowing nothing. Deleted; the re-export is the behavior.
|
|
60
65
|
|
|
61
66
|
export const tablesCmd = (wrap) => buildDevCmd('tables', 'sys_db_object', ['table', 't'], ['name', 'label', 'super_class', 'create_access_controls'], wrap, {
|
|
62
67
|
singular: 'table',
|
|
@@ -98,28 +103,27 @@ export { aliasesCmd } from './aliases.js';
|
|
|
98
103
|
export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript', 'cs'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
|
|
99
104
|
|
|
100
105
|
// ── Automation: Async ──
|
|
101
|
-
export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
|
|
102
|
-
export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
|
|
103
|
-
export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'when=async' });
|
|
104
|
-
export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
|
|
106
|
+
export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true, devAlias: false });
|
|
107
|
+
export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true, devAlias: false });
|
|
108
|
+
export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'when=async', devAlias: false });
|
|
109
|
+
export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true, devAlias: false });
|
|
105
110
|
|
|
106
111
|
// ── Automation: In Memory ──
|
|
107
|
-
export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_ws_definition', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
|
|
108
|
-
export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
|
|
112
|
+
export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_ws_definition', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true, devAlias: false });
|
|
113
|
+
export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true, devAlias: false });
|
|
109
114
|
|
|
110
115
|
// ── Automation: Inbound ──
|
|
111
|
-
export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_in_email_action', ['emails'], ['name', 'active', 'type', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
|
|
116
|
+
export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_in_email_action', ['emails'], ['name', 'active', 'type', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true, devAlias: false });
|
|
112
117
|
|
|
113
118
|
// ── Automation: Outbound ──
|
|
114
119
|
export { restmessageCmd } from './restmessage.js';
|
|
115
|
-
export const restmethodsCmd = (wrap) => buildDevCmd('restmethods', 'sys_rest_message_fn', ['restmethod'], ['name', 'http_method', 'active', 'sys_scope'], wrap, { singular: 'REST method', scopeValidation: true });
|
|
116
120
|
export { soapmessagesCmd } from './soapmessages.js';
|
|
117
121
|
|
|
118
122
|
// ── UX: Core UI ──
|
|
119
|
-
export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
|
|
123
|
+
export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true, devAlias: false });
|
|
120
124
|
|
|
121
125
|
// ── UX: Workspaces ──
|
|
122
|
-
export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
|
|
123
|
-
export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
|
|
126
|
+
export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true, devAlias: false });
|
|
127
|
+
export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true, devAlias: false });
|
|
124
128
|
export { uipoliciesCmd } from './uipolicies.js';
|
|
125
129
|
export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'catalog_item', 'variable_set', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
|
|
@@ -2,4 +2,4 @@ import { buildDevCmd } from './_generic.js';
|
|
|
2
2
|
|
|
3
3
|
// Before-query business rules — applies filters to GlideRecord queries before they execute.
|
|
4
4
|
// These are sys_script records with order < 100.
|
|
5
|
-
export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'when=before' });
|
|
5
|
+
export const b4rulesCmd = (wrap) => buildDevCmd('b4rules', 'sys_script', ['b4rule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'before-query business rule', scopeValidation: true, extraQuery: 'when=before', devAlias: false });
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// to its own picker sessions.
|
|
12
12
|
|
|
13
13
|
import { getStringField, interactiveList, assertSafeExactMatch } from '../../helpers.js';
|
|
14
|
+
import { declareCapabilities } from '../../capabilities.js';
|
|
15
|
+
|
|
16
|
+
// `domains set` writes the chosen domain into the profile config.
|
|
17
|
+
declareCapabilities('domains', { mutationSubcommands: ['set'], devAlias: true });
|
|
14
18
|
|
|
15
19
|
const DETAIL_FIELDS = 'sys_id,name,parent,sys_domain_path,description,active,sys_created_on,sys_updated_on';
|
|
16
20
|
|
package/src/commands/dev/eval.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
3
|
+
import { declareCapabilities } from '../../capabilities.js';
|
|
4
|
+
|
|
5
|
+
// The whole command is a mutation surface (runs arbitrary server-side JS) —
|
|
6
|
+
// declared as `['']` so both `jsn eval` and `jsn dev eval` are gated.
|
|
7
|
+
declareCapabilities('eval', { mutationSubcommands: [''], devAlias: true });
|
|
2
8
|
|
|
3
9
|
export function evalCmd(wrap) {
|
|
4
10
|
return {
|
|
@@ -6,27 +12,71 @@ export function evalCmd(wrap) {
|
|
|
6
12
|
describe: 'Execute background scripts on the instance',
|
|
7
13
|
builder: (yargs) => {
|
|
8
14
|
return yargs
|
|
9
|
-
.option('script', { alias: 's', type: 'string', describe:
|
|
15
|
+
.option('script', { alias: 's', type: 'string', describe: 'JavaScript code to execute (single-quote the script, double quotes inside: --script \'gs.log("Hello")\')' })
|
|
10
16
|
.option('file', { alias: 'f', type: 'string', describe: 'Read script from file (avoids shell quoting issues)' })
|
|
11
|
-
.option('
|
|
17
|
+
.option('stdin', { type: 'boolean', describe: 'Read script from stdin (pipe-friendly, e.g. cat script.js | jsn eval --stdin)' })
|
|
18
|
+
.option('scope', { alias: null, type: 'string', describe: 'Scope name or sys_id to run the script under (default: active scope from banner)' })
|
|
19
|
+
// Script-mode flags (issue #177) — map to the Script Background form checkboxes
|
|
20
|
+
.option('rollback', { type: 'boolean', default: true, describe: 'Record rollback context (--no-rollback to skip)' })
|
|
21
|
+
.option('sandbox', { type: 'boolean', default: false, describe: 'Sandbox mode (KittyScript evaluator, single expression only — no DB writes)' })
|
|
22
|
+
.option('scriptlet', { type: 'boolean', default: false, describe: 'Run as scriptlet with global server-side objects' })
|
|
23
|
+
.option('quota-managed-transaction', { type: 'boolean', default: true, describe: 'Managed transaction limits (--no-quota-managed-transaction to skip)' });
|
|
12
24
|
},
|
|
13
25
|
handler: wrap(async (argv, app) => {
|
|
14
26
|
let script;
|
|
15
27
|
let scopeSysId = null;
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
// Resolve execution scope: explicit --scope wins; otherwise use the
|
|
30
|
+
// active scope from apps.current_app (same source as the banner);
|
|
31
|
+
// fall back to global only when no active scope exists.
|
|
32
|
+
let scope = argv.scope || '';
|
|
33
|
+
if (!scope) {
|
|
34
|
+
try {
|
|
35
|
+
const user = await getCurrentUser(app.sdk);
|
|
36
|
+
if (user && user.sys_id) {
|
|
37
|
+
const currentApp = await getCurrentApplication(app.sdk, user.sys_id);
|
|
38
|
+
if (currentApp && currentApp.scope && currentApp.scope !== 'global') {
|
|
39
|
+
scope = currentApp.scope;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore — fall back to global
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (scope) {
|
|
48
|
+
scopeSysId = await app.sdk.resolveScope(scope);
|
|
19
49
|
if (!scopeSysId) {
|
|
20
|
-
throw new Error(`Scope not found: ${
|
|
50
|
+
throw new Error(`Scope not found: ${scope}`);
|
|
21
51
|
}
|
|
22
52
|
}
|
|
23
53
|
|
|
24
54
|
if (argv.file) {
|
|
25
55
|
script = fs.readFileSync(argv.file, 'utf-8');
|
|
56
|
+
} else if (argv.stdin) {
|
|
57
|
+
script = await new Promise((resolve, reject) => {
|
|
58
|
+
let data = '';
|
|
59
|
+
process.stdin.setEncoding('utf-8');
|
|
60
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
61
|
+
process.stdin.on('end', () => resolve(data));
|
|
62
|
+
process.stdin.on('error', reject);
|
|
63
|
+
});
|
|
26
64
|
} else if (argv.script) {
|
|
27
65
|
script = argv.script;
|
|
28
66
|
} else {
|
|
29
|
-
throw new Error('--script or --
|
|
67
|
+
throw new Error('--script, --file, or --stdin is required');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Sandbox mode uses the KittyScript evaluator, which only accepts a
|
|
71
|
+
// single expression — multi-statement scripts fail server-side. Warn
|
|
72
|
+
// early, on the user's raw script (before the $scopeSysId prepend).
|
|
73
|
+
if (argv.sandbox) {
|
|
74
|
+
const statements = script.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('//'));
|
|
75
|
+
const multiLine = statements.length > 1;
|
|
76
|
+
const hasTerminator = /;[\s\S]*\S/.test(script);
|
|
77
|
+
if (multiLine || hasTerminator) {
|
|
78
|
+
process.stderr.write('Warning: --sandbox runs a single expression (KittyScript). Multi-statement scripts will fail.\n\n');
|
|
79
|
+
}
|
|
30
80
|
}
|
|
31
81
|
|
|
32
82
|
if (scopeSysId) {
|
|
@@ -36,10 +86,18 @@ export function evalCmd(wrap) {
|
|
|
36
86
|
const warning = scopeSysId
|
|
37
87
|
? ` ⚠️ Records without explicit sys_scope will land in global. Use $scopeSysId variable.`
|
|
38
88
|
: '';
|
|
39
|
-
|
|
89
|
+
// Pass the resolved sys_id (not the name) — sys.scripts.do only
|
|
90
|
+
// honors selectable scope values, which are sys_ids.
|
|
91
|
+
const output = await app.sdk.executeScript(script, scopeSysId || '', {
|
|
92
|
+
rollback: argv.rollback,
|
|
93
|
+
quotaManagedTransaction: argv.quotaManagedTransaction,
|
|
94
|
+
sandbox: argv.sandbox,
|
|
95
|
+
scriptlet: argv.scriptlet,
|
|
96
|
+
});
|
|
40
97
|
app.ok({
|
|
41
98
|
script,
|
|
42
99
|
output,
|
|
100
|
+
scope: scope || 'global',
|
|
43
101
|
instance: app.getEffectiveInstance(),
|
|
44
102
|
}, {
|
|
45
103
|
summary: 'Script executed' + warning,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { formatRecordForDisplay, getStringField, interactiveList } from '../../helpers.js';
|
|
2
|
+
import { declareCapabilities } from '../../capabilities.js';
|
|
3
|
+
|
|
4
|
+
declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete'], devAlias: true });
|
|
2
5
|
|
|
3
6
|
export function flowsCmd(wrap) {
|
|
4
7
|
return {
|