@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
package/src/commands/catalog.js
CHANGED
|
@@ -1,325 +1,212 @@
|
|
|
1
|
-
|
|
2
|
-
// NOTE: This is an AI-friendly high-level command that wraps sc_cat_item + item_option_new
|
|
3
|
-
|
|
4
|
-
import { resolveItemOptionType } from '../helpers.js';
|
|
1
|
+
import { getStringField, interactiveList } from '../helpers.js';
|
|
5
2
|
|
|
6
3
|
export function catalogCmd(wrap) {
|
|
7
4
|
return {
|
|
8
|
-
command: '
|
|
9
|
-
aliases: ['
|
|
10
|
-
describe: 'Manage Service Catalog items
|
|
5
|
+
command: 'catalogitems [subcommand]',
|
|
6
|
+
aliases: ['ci'],
|
|
7
|
+
describe: 'Manage Service Catalog items',
|
|
11
8
|
builder: (yargs) => {
|
|
12
9
|
return yargs
|
|
13
|
-
// ── create-item: Create a new catalog item definition ──
|
|
14
10
|
.command({
|
|
15
|
-
command: 'create
|
|
16
|
-
|
|
11
|
+
command: 'create',
|
|
12
|
+
aliases: ['new'],
|
|
13
|
+
describe: 'Create a catalog item',
|
|
17
14
|
builder: (y) => y
|
|
18
|
-
.option('name', { alias: 'n', type: 'string', demandOption: true
|
|
19
|
-
.option('short-description', { alias: 'd', type: 'string'
|
|
20
|
-
.option('
|
|
21
|
-
.option('category', { alias: 'c', type: 'string', describe: 'Category name (created if missing)' })
|
|
22
|
-
.option('variable', { alias: 'v', type: 'array', describe: 'Variable definition in format "name:type:label" or "name:type" (e.g. "reason:multilinetext:Reason" or "start_date:date")' })
|
|
23
|
-
.option('variables', { type: 'string', describe: 'JSON object of variable values for submit_produce API (e.g. \'{"quantity":"100","reason":"Need more"}\')' })
|
|
24
|
-
.option('update-set', { type: 'string', describe: 'Update set sys_id or name to capture changes' }),
|
|
15
|
+
.option('name', { alias: 'n', type: 'string', demandOption: true })
|
|
16
|
+
.option('short-description', { alias: 'd', type: 'string' })
|
|
17
|
+
.option('category', { alias: 'c', type: 'string' }),
|
|
25
18
|
handler: wrap(async (argv, app) => {
|
|
26
|
-
|
|
27
|
-
if (argv.variables) {
|
|
28
|
-
let varsObj;
|
|
29
|
-
try {
|
|
30
|
-
varsObj = JSON.parse(argv.variables);
|
|
31
|
-
} catch {
|
|
32
|
-
throw new Error('--variables must be a valid JSON object, e.g. {"reason":"Need this"}');
|
|
33
|
-
}
|
|
34
|
-
// Resolve the catalog item sys_id from name if needed
|
|
35
|
-
let itemSysID = argv.name;
|
|
36
|
-
if (!itemSysID.match(/^[a-f0-9]{32}$/i)) {
|
|
37
|
-
const params = new URLSearchParams();
|
|
38
|
-
params.set('sysparm_query', `name=${itemSysID}`);
|
|
39
|
-
params.set('sysparm_limit', '1');
|
|
40
|
-
params.set('sysparm_fields', 'sys_id');
|
|
41
|
-
const items = await app.sdk.list('sc_cat_item', params);
|
|
42
|
-
if (items.length === 0) throw new Error(`Catalog item not found: ${itemSysID}`);
|
|
43
|
-
const item = items[0];
|
|
44
|
-
itemSysID = item.sys_id?.value || item.sys_id;
|
|
45
|
-
}
|
|
46
|
-
// Submit via submit_produce API
|
|
47
|
-
const endpoint = `${app.sdk.baseURL}/api/sn_sc/servicecatalog/items/${itemSysID}/submit_produce`;
|
|
48
|
-
const body = { variables: varsObj };
|
|
49
|
-
const result = await app.sdk.request(endpoint, {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
body: JSON.stringify(body),
|
|
52
|
-
});
|
|
53
|
-
const reqID = result?.result?.sys_id || result?.result?.number || '';
|
|
54
|
-
app.ok({
|
|
55
|
-
requested_item: reqID,
|
|
56
|
-
item_id: itemSysID,
|
|
57
|
-
variables: varsObj,
|
|
58
|
-
}, {
|
|
59
|
-
summary: `Request created: ${result?.result?.number || reqID}`,
|
|
60
|
-
breadcrumbs: [
|
|
61
|
-
{ action: 'view', cmd: `jsn requests show ${reqID}`, description: 'View the request' },
|
|
62
|
-
],
|
|
63
|
-
});
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Original create-item flow: create a new catalog item definition
|
|
68
|
-
const name = argv.name;
|
|
69
|
-
const shortDesc = argv['short-description'] || '';
|
|
70
|
-
const description = argv.description || shortDesc;
|
|
19
|
+
app.requireInstance();
|
|
71
20
|
let categoryID = '';
|
|
72
|
-
|
|
73
|
-
// Resolve or create category
|
|
74
21
|
if (argv.category) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const cats = await app.sdk.list('sc_category',
|
|
80
|
-
if (cats.length > 0)
|
|
81
|
-
|
|
82
|
-
} else {
|
|
83
|
-
const newCat = await app.sdk.create('sc_category', {
|
|
84
|
-
title: argv.category,
|
|
85
|
-
description: `Category for ${argv.category}`,
|
|
86
|
-
});
|
|
87
|
-
categoryID = newCat.sys_id?.value || newCat.sys_id;
|
|
88
|
-
}
|
|
22
|
+
const cp = new URLSearchParams();
|
|
23
|
+
cp.set('sysparm_query', `title=${argv.category}`);
|
|
24
|
+
cp.set('sysparm_limit', '1');
|
|
25
|
+
cp.set('sysparm_fields', 'sys_id');
|
|
26
|
+
const cats = await app.sdk.list('sc_category', cp);
|
|
27
|
+
if (cats.length > 0) categoryID = cats[0].sys_id?.value || cats[0].sys_id;
|
|
28
|
+
else { const nc = await app.sdk.create('sc_category', { title: argv.category }); categoryID = nc.sys_id?.value || nc.sys_id; }
|
|
89
29
|
}
|
|
90
|
-
|
|
91
|
-
// Create the catalog item
|
|
92
30
|
const item = await app.sdk.create('sc_cat_item', {
|
|
93
|
-
name,
|
|
94
|
-
short_description:
|
|
95
|
-
description,
|
|
31
|
+
name: argv.name,
|
|
32
|
+
short_description: argv['short-description'] || '',
|
|
96
33
|
category: categoryID || undefined,
|
|
97
34
|
active: true,
|
|
98
35
|
type: 'item',
|
|
99
|
-
stage: 'requested',
|
|
100
|
-
});
|
|
101
|
-
const itemID = item.sys_id?.value || item.sys_id;
|
|
102
|
-
|
|
103
|
-
// Parse and create variables
|
|
104
|
-
const varDefs = [];
|
|
105
|
-
if (argv.variable) {
|
|
106
|
-
for (const v of argv.variable) {
|
|
107
|
-
const parts = String(v).split(':');
|
|
108
|
-
if (parts.length >= 2) {
|
|
109
|
-
const typeName = parts.length >= 2 ? parts[parts.length - 2] : '';
|
|
110
|
-
const label = parts.length >= 3 ? parts[parts.length - 1] : parts[0];
|
|
111
|
-
const typeID = resolveItemOptionType(typeName);
|
|
112
|
-
const sysName = parts[0].replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
|
|
113
|
-
varDefs.push({
|
|
114
|
-
name: sysName,
|
|
115
|
-
question_text: label,
|
|
116
|
-
type: typeID,
|
|
117
|
-
order: varDefs.length * 100 + 100,
|
|
118
|
-
cat_item: itemID,
|
|
119
|
-
mandatory: true,
|
|
120
|
-
active: true,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
for (const vd of varDefs) {
|
|
126
|
-
try {
|
|
127
|
-
await app.sdk.create('item_option_new', vd);
|
|
128
|
-
} catch {
|
|
129
|
-
// variable creation failure is non-fatal
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
app.ok({
|
|
135
|
-
sys_id: itemID,
|
|
136
|
-
name,
|
|
137
|
-
category: argv.category || null,
|
|
138
|
-
variables_created: varDefs.length,
|
|
139
|
-
update_set: argv['update-set'] || null,
|
|
140
|
-
instance_url: app.getEffectiveInstance(),
|
|
141
|
-
}, {
|
|
142
|
-
summary: `Created catalog item: ${name} (${varDefs.length} variable(s))`,
|
|
143
|
-
breadcrumbs: [
|
|
144
|
-
{ action: 'view', cmd: `jsn records get --table sc_cat_item --sys-id ${itemID}`, description: 'View the catalog item' },
|
|
145
|
-
{ action: 'edit', cmd: `jsn catalog update-item ${itemID} --name "${name}"`, description: 'Edit the catalog item' },
|
|
146
|
-
],
|
|
147
36
|
});
|
|
37
|
+
app.ok(item, { summary: `Created: ${argv.name}` });
|
|
148
38
|
}),
|
|
149
39
|
})
|
|
150
|
-
// ── items: List and show catalog items ──
|
|
151
40
|
.command({
|
|
152
|
-
command: '
|
|
153
|
-
aliases: ['
|
|
154
|
-
describe: '
|
|
41
|
+
command: 'list',
|
|
42
|
+
aliases: ['ls'],
|
|
43
|
+
describe: 'List catalog items',
|
|
155
44
|
builder: (y) => y
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
|
|
162
|
-
.option('category', { type: 'string', describe: 'Filter by category' }),
|
|
163
|
-
handler: wrap(async (argv, app) => {
|
|
164
|
-
let query = 'ORDERBYname';
|
|
165
|
-
if (argv.category) query = `category.titleLIKE${argv.category}^${query}`;
|
|
166
|
-
const params = new URLSearchParams();
|
|
167
|
-
params.set('sysparm_query', query);
|
|
168
|
-
params.set('sysparm_limit', String(argv.limit));
|
|
169
|
-
params.set('sysparm_display_value', 'all');
|
|
170
|
-
params.set('sysparm_fields', 'sys_id,name,short_description,category,active');
|
|
171
|
-
const items = await app.sdk.list('sc_cat_item', params);
|
|
172
|
-
app.ok({
|
|
173
|
-
table: 'sc_cat_item',
|
|
174
|
-
count: items.length,
|
|
175
|
-
columns: ['name', 'short_description', 'category', 'active'],
|
|
176
|
-
records: items.map(r => ({
|
|
177
|
-
sys_id: r.sys_id?.value || r.sys_id,
|
|
178
|
-
name: r.name?.display_value || r.name,
|
|
179
|
-
short_description: r.short_description?.display_value || r.short_description || '',
|
|
180
|
-
category: r.category?.display_value || '',
|
|
181
|
-
active: r.active?.display_value || r.active,
|
|
182
|
-
})),
|
|
183
|
-
context: { instance_url: app.getEffectiveInstance() },
|
|
184
|
-
}, { summary: `${items.length} catalog item(s)` });
|
|
185
|
-
}),
|
|
186
|
-
})
|
|
187
|
-
.command({
|
|
188
|
-
command: 'show <id>',
|
|
189
|
-
describe: 'Show a catalog item with variables and variable sets',
|
|
190
|
-
handler: wrap(async (argv, app) => {
|
|
191
|
-
let itemID = argv.id;
|
|
192
|
-
// Resolve name to sys_id
|
|
193
|
-
if (!itemID.match(/^[a-f0-9]{32}$/i)) {
|
|
194
|
-
const params = new URLSearchParams();
|
|
195
|
-
params.set('sysparm_query', `name=${itemID}`);
|
|
196
|
-
params.set('sysparm_limit', '1');
|
|
197
|
-
params.set('sysparm_fields', 'sys_id,name');
|
|
198
|
-
const items = await app.sdk.list('sc_cat_item', params);
|
|
199
|
-
if (items.length === 0) throw new Error(`Catalog item not found: ${itemID}`);
|
|
200
|
-
itemID = items[0].sys_id?.value || items[0].sys_id;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Get the catalog item
|
|
204
|
-
const item = await app.sdk.get('sc_cat_item', itemID);
|
|
205
|
-
|
|
206
|
-
// Get variables (item_option_new) for this catalog item
|
|
207
|
-
const varParams = new URLSearchParams();
|
|
208
|
-
varParams.set('sysparm_query', `cat_item=${itemID}^active=true`);
|
|
209
|
-
varParams.set('sysparm_limit', '100');
|
|
210
|
-
varParams.set('sysparm_display_value', 'all');
|
|
211
|
-
varParams.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
|
|
212
|
-
const variables = await app.sdk.list('item_option_new', varParams);
|
|
45
|
+
.option('limit', { alias: 'l', type: 'number', default: 50 })
|
|
46
|
+
.option('category', { type: 'string' }),
|
|
47
|
+
handler: wrap(async (argv, app) => {
|
|
48
|
+
app.requireInstance();
|
|
49
|
+
const query = argv.category ? `category.titleLIKE${argv.category}` : '';
|
|
213
50
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!setVars.has(vs)) {
|
|
229
|
-
setVars.set(vs, { name: vs, label: vs, variables: [] });
|
|
230
|
-
}
|
|
231
|
-
setVars.get(vs).variables.push(entry);
|
|
232
|
-
} else {
|
|
233
|
-
standaloneVars.push(entry);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
51
|
+
const picked = await interactiveList({
|
|
52
|
+
app,
|
|
53
|
+
table: 'sc_cat_item',
|
|
54
|
+
singular: 'catalog item',
|
|
55
|
+
columns: ['name', 'short_description', 'category', 'active'],
|
|
56
|
+
limit: argv.limit,
|
|
57
|
+
query,
|
|
58
|
+
labelField: 'name',
|
|
59
|
+
formatLabel: (r) => {
|
|
60
|
+
const n = getStringField(r, 'name') || '';
|
|
61
|
+
const c = getStringField(r, 'category') || '';
|
|
62
|
+
return c ? `${n} [${c}]` : n;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
236
65
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
setData.label = vsRecord.label?.value || vsRecord.label || setData.name;
|
|
243
|
-
setData.order = vsRecord.order?.display_value || vsRecord.order;
|
|
244
|
-
} catch {
|
|
245
|
-
// Can't resolve, keep sys_id as name
|
|
246
|
-
}
|
|
247
|
-
}
|
|
66
|
+
if (picked === undefined) return;
|
|
67
|
+
if (picked) {
|
|
68
|
+
await showItem(app, getStringField(picked, 'sys_id') || '');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
248
71
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
variables: {
|
|
257
|
-
standalone: standaloneVars,
|
|
258
|
-
variable_sets: Array.from(setVars.values()),
|
|
259
|
-
},
|
|
260
|
-
instance_url: app.getEffectiveInstance(),
|
|
261
|
-
}, {
|
|
262
|
-
summary: `${item.name?.display_value || item.name} — ${standaloneVars.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0)} variable(s)`,
|
|
263
|
-
});
|
|
264
|
-
}),
|
|
265
|
-
})
|
|
266
|
-
.demandCommand(1, 'Specify a subcommand: list or show'),
|
|
267
|
-
handler: () => {},
|
|
268
|
-
})
|
|
269
|
-
// ── list-items: Legacy alias ──
|
|
270
|
-
.command({
|
|
271
|
-
command: 'list-items',
|
|
272
|
-
aliases: ['ls'],
|
|
273
|
-
describe: 'List catalog items (use "catalog items list" instead)',
|
|
274
|
-
builder: (y) => y
|
|
275
|
-
.option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
|
|
276
|
-
.option('category', { type: 'string', describe: 'Filter by category' }),
|
|
277
|
-
handler: wrap(async (argv, app) => {
|
|
278
|
-
let query = 'ORDERBYname';
|
|
279
|
-
if (argv.category) query = `category.titleLIKE${argv.category}^${query}`;
|
|
280
|
-
const params = new URLSearchParams();
|
|
281
|
-
params.set('sysparm_query', query);
|
|
282
|
-
params.set('sysparm_limit', String(argv.limit));
|
|
283
|
-
params.set('sysparm_display_value', 'all');
|
|
284
|
-
params.set('sysparm_fields', 'sys_id,name,short_description,category,active');
|
|
285
|
-
const items = await app.sdk.list('sc_cat_item', params);
|
|
72
|
+
// Fallback — non-TTY
|
|
73
|
+
const p = new URLSearchParams();
|
|
74
|
+
p.set('sysparm_query', query ? `${query}^ORDERBYname` : 'ORDERBYname');
|
|
75
|
+
p.set('sysparm_limit', String(argv.limit));
|
|
76
|
+
p.set('sysparm_display_value', 'all');
|
|
77
|
+
p.set('sysparm_fields', 'name,short_description,category,active');
|
|
78
|
+
const records = await app.sdk.list('sc_cat_item', p);
|
|
286
79
|
app.ok({
|
|
287
80
|
table: 'sc_cat_item',
|
|
288
|
-
count:
|
|
81
|
+
count: records.length,
|
|
289
82
|
columns: ['name', 'short_description', 'category', 'active'],
|
|
290
|
-
records
|
|
291
|
-
sys_id: r.sys_id?.value || r.sys_id,
|
|
292
|
-
name: r.name?.display_value || r.name,
|
|
293
|
-
short_description: r.short_description?.display_value || r.short_description || '',
|
|
294
|
-
category: r.category?.display_value || '',
|
|
295
|
-
active: r.active?.display_value || r.active,
|
|
296
|
-
})),
|
|
83
|
+
records,
|
|
297
84
|
context: { instance_url: app.getEffectiveInstance() },
|
|
298
|
-
}, { summary: `${
|
|
85
|
+
}, { summary: `${records.length} catalog item(s)` });
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
.command({
|
|
89
|
+
command: 'show <id>',
|
|
90
|
+
aliases: ['get'],
|
|
91
|
+
describe: 'Show a catalog item',
|
|
92
|
+
handler: wrap(async (argv, app) => {
|
|
93
|
+
app.requireInstance();
|
|
94
|
+
let id = argv.id;
|
|
95
|
+
if (!/^[a-f0-9]{32}$/i.test(id)) {
|
|
96
|
+
const p = new URLSearchParams();
|
|
97
|
+
p.set('sysparm_query', `name=${id}`);
|
|
98
|
+
p.set('sysparm_limit', '1');
|
|
99
|
+
p.set('sysparm_fields', 'sys_id');
|
|
100
|
+
const results = await app.sdk.list('sc_cat_item', p);
|
|
101
|
+
if (results.length === 0) throw new Error(`Not found: ${id}`);
|
|
102
|
+
id = results[0].sys_id?.value || results[0].sys_id;
|
|
103
|
+
}
|
|
104
|
+
const item = await app.sdk.get('sc_cat_item', id);
|
|
105
|
+
await showItem(app, id);
|
|
299
106
|
}),
|
|
300
107
|
});
|
|
301
108
|
},
|
|
302
|
-
handler: (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.log('\nExamples:');
|
|
310
|
-
console.log(' jsn catalog create-item "Request PTO" \\');
|
|
311
|
-
console.log(' --category "Time Off" \\');
|
|
312
|
-
console.log(' --short-description "Submit a PTO request" \\');
|
|
313
|
-
console.log(' --variable "start_date:date:Start Date" \\');
|
|
314
|
-
console.log(' --variable "type:select:Type" \\');
|
|
315
|
-
console.log(' --variable "reason:multilinetext:Reason/Notes"');
|
|
316
|
-
console.log('');
|
|
317
|
-
console.log(' # Submit a request via submit_produce:');
|
|
318
|
-
console.log(' jsn catalog create-item "Minecraft Coins" \\');
|
|
319
|
-
console.log(' --variables \'{"quantity":"100","reason":"Server upgrade"}\'');
|
|
320
|
-
console.log('\nVariable types: string, multilinetext, select, date, datetime, reference, checkbox, email');
|
|
321
|
-
console.log('\nRun "jsn catalog <command> --help" for details.');
|
|
322
|
-
}
|
|
109
|
+
handler: () => {
|
|
110
|
+
console.log('Service Catalog management.');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('Commands:');
|
|
113
|
+
console.log(' create Create a catalog item');
|
|
114
|
+
console.log(' list Browse catalog items');
|
|
115
|
+
console.log(' show <id> Show catalog item');
|
|
323
116
|
},
|
|
324
117
|
};
|
|
325
118
|
}
|
|
119
|
+
|
|
120
|
+
async function showItem(app, sysID) {
|
|
121
|
+
const item = await app.sdk.get('sc_cat_item', sysID);
|
|
122
|
+
|
|
123
|
+
// Fetch variables
|
|
124
|
+
const vp = new URLSearchParams();
|
|
125
|
+
vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
|
|
126
|
+
vp.set('sysparm_limit', '100');
|
|
127
|
+
vp.set('sysparm_display_value', 'all');
|
|
128
|
+
vp.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
|
|
129
|
+
const variables = await app.sdk.list('item_option_new', vp);
|
|
130
|
+
|
|
131
|
+
const standalone = [];
|
|
132
|
+
const setVars = new Map();
|
|
133
|
+
for (const v of variables) {
|
|
134
|
+
const vs = getStringField(v, 'variable_set');
|
|
135
|
+
const entry = {
|
|
136
|
+
name: getStringField(v, 'name'),
|
|
137
|
+
question_text: getStringField(v, 'question_text'),
|
|
138
|
+
type: getStringField(v, 'type'),
|
|
139
|
+
mandatory: getStringField(v, 'mandatory'),
|
|
140
|
+
};
|
|
141
|
+
if (vs) {
|
|
142
|
+
if (!setVars.has(vs)) setVars.set(vs, { name: vs, variables: [] });
|
|
143
|
+
setVars.get(vs).variables.push(entry);
|
|
144
|
+
} else {
|
|
145
|
+
standalone.push(entry);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const totalVars = standalone.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0);
|
|
150
|
+
|
|
151
|
+
const lines = [];
|
|
152
|
+
lines.push(getStringField(item, 'name'));
|
|
153
|
+
if (getStringField(item, 'short_description')) lines.push(` ${getStringField(item, 'short_description')}`);
|
|
154
|
+
lines.push(` Category: ${getStringField(item, 'category') || '(none)'}`);
|
|
155
|
+
lines.push(` Active: ${getStringField(item, 'active') || 'false'}`);
|
|
156
|
+
lines.push(` URL: ${app.getEffectiveInstance()}/sc_cat_item.do?sys_id=${sysID}`);
|
|
157
|
+
|
|
158
|
+
// Flow / Workflow / Execution Plan
|
|
159
|
+
const flowName = getStringField(item, 'flow_designer_flow');
|
|
160
|
+
const flowID = item.flow_designer_flow?.value || '';
|
|
161
|
+
const wfName = getStringField(item, 'workflow');
|
|
162
|
+
const wfID = item.workflow?.value || '';
|
|
163
|
+
const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan');
|
|
164
|
+
|
|
165
|
+
if (flowID) {
|
|
166
|
+
if (flowName === flowID) {
|
|
167
|
+
try {
|
|
168
|
+
const flowRec = await app.sdk.get('sys_hub_flow', flowID);
|
|
169
|
+
lines.push(` Flow: ${getStringField(flowRec, 'name') || flowID}`);
|
|
170
|
+
} catch {
|
|
171
|
+
lines.push(` Flow: ${flowName}`);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(` Flow: ${flowName}`);
|
|
175
|
+
}
|
|
176
|
+
lines.push(` → jsn flows show ${flowID}`);
|
|
177
|
+
} else if (wfName) {
|
|
178
|
+
lines.push(` Workflow: ${wfName}`);
|
|
179
|
+
if (wfID) lines.push(` → jsn workflows show ${wfID}`);
|
|
180
|
+
} else if (planName) {
|
|
181
|
+
const planID = item.delivery_plan?.value || item.execution_plan?.value || '';
|
|
182
|
+
if (planName === planID && planID) {
|
|
183
|
+
try {
|
|
184
|
+
const planRec = await app.sdk.get('delivery_plan', planID);
|
|
185
|
+
const realName = getStringField(planRec, 'name') || getStringField(planRec, 'short_description') || planID;
|
|
186
|
+
lines.push(` Execution Plan: ${realName}`);
|
|
187
|
+
lines.push(` → jsn records get --table delivery_plan --sys-id ${planID}`);
|
|
188
|
+
} catch {
|
|
189
|
+
lines.push(` Execution Plan: ${planName}`);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
lines.push(` Execution Plan: ${planName}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (totalVars > 0) {
|
|
197
|
+
lines.push(` Variables (${totalVars}):`);
|
|
198
|
+
for (const v of standalone) {
|
|
199
|
+
const flags = String(v.mandatory) === 'true' ? ' [required]' : '';
|
|
200
|
+
lines.push(` ${v.question_text || v.name} ${v.type || ''}${flags}`);
|
|
201
|
+
}
|
|
202
|
+
for (const [_, sd] of setVars) {
|
|
203
|
+
lines.push(` [${sd.name}]`);
|
|
204
|
+
for (const v of sd.variables) {
|
|
205
|
+
const flags = String(v.mandatory) === 'true' ? ' [required]' : '';
|
|
206
|
+
lines.push(` ${v.question_text || v.name} ${v.type || ''}${flags}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(lines.join('\n'));
|
|
212
|
+
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { formatRecordForDisplay, getStringField, isHexString, parseDataArg } from '../../helpers.js';
|
|
4
4
|
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
5
|
-
import
|
|
6
|
-
import { search } from '@inquirer/prompts';
|
|
5
|
+
import { paginatedSearch } from '../../paginated-search.js';
|
|
7
6
|
import { isTTY, FormatAuto } from '../../output.js';
|
|
8
7
|
|
|
9
8
|
function vowelArticle(word) {
|
|
@@ -81,6 +80,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
81
80
|
const singular = toSingular(name, opts.singular);
|
|
82
81
|
const readOnly = opts.readOnly || false;
|
|
83
82
|
const scopeValidation = opts.scopeValidation || false;
|
|
83
|
+
const extraQuery = opts.extraQuery || '';
|
|
84
84
|
const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
|
|
85
85
|
const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
|
|
86
86
|
const crumbs = [
|
|
@@ -110,65 +110,52 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
110
110
|
const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
|
|
111
111
|
const limit = argv.limit;
|
|
112
112
|
|
|
113
|
-
// Interactive picker
|
|
113
|
+
// Interactive picker with pagination
|
|
114
114
|
const interactive = isTTY(process.stdout) && isTTY(process.stdin);
|
|
115
115
|
if (interactive && app.output.getFormat() === FormatAuto && !query) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
pickerParams.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
|
|
116
|
+
// Get total count
|
|
117
|
+
let totalCount = 0;
|
|
118
|
+
try {
|
|
119
|
+
totalCount = await app.sdk.aggregateCount(table, extraQuery || '');
|
|
120
|
+
} catch { /* non-critical */ }
|
|
122
121
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
const source = async (term, offset, { 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 => {
|
|
135
|
+
const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
|
|
136
|
+
const scope = getStringField(r, 'sys_scope') || '';
|
|
137
|
+
let label = recordName;
|
|
138
|
+
if (scope && scope !== 'global') label += ` [${scope}]`;
|
|
139
|
+
return { name: label, value: recordName };
|
|
135
140
|
});
|
|
136
|
-
|
|
137
|
-
}
|
|
141
|
+
};
|
|
138
142
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return { name: label, value: recordName };
|
|
143
|
+
const selectedName = await paginatedSearch({
|
|
144
|
+
message: `Select ${vowelArticle(singular)} ${singular}`,
|
|
145
|
+
pageSize: 10,
|
|
146
|
+
totalCount,
|
|
147
|
+
source,
|
|
145
148
|
});
|
|
146
149
|
|
|
147
|
-
let selectedName;
|
|
148
|
-
try {
|
|
149
|
-
selectedName = await search({
|
|
150
|
-
message: `Select ${vowelArticle(singular)} ${singular}:`,
|
|
151
|
-
source: async (input) => {
|
|
152
|
-
if (!input) return choices;
|
|
153
|
-
const term = input.toLowerCase();
|
|
154
|
-
return choices.filter(c => c.name.toLowerCase().includes(term));
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
} catch (err) {
|
|
158
|
-
if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
throw err;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
150
|
if (selectedName) {
|
|
151
|
+
const recordName = typeof selectedName === 'object' ? selectedName.value : selectedName;
|
|
165
152
|
const showParams = new URLSearchParams();
|
|
166
|
-
showParams.set('sysparm_query', `name=${
|
|
153
|
+
showParams.set('sysparm_query', `name=${recordName}`);
|
|
167
154
|
showParams.set('sysparm_display_value', 'all');
|
|
168
155
|
showParams.set('sysparm_limit', '1');
|
|
169
156
|
const showRecords = await app.sdk.list(table, showParams);
|
|
170
157
|
if (showRecords.length === 0) {
|
|
171
|
-
throw new Error(`${singular} not found: ${
|
|
158
|
+
throw new Error(`${singular} not found: ${recordName}`);
|
|
172
159
|
}
|
|
173
160
|
const record = showRecords[0];
|
|
174
161
|
record._context = { instance_url: app.getEffectiveInstance(), table };
|
|
@@ -176,9 +163,9 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
176
163
|
await opts.onShow(record, app);
|
|
177
164
|
}
|
|
178
165
|
app.ok(record, {
|
|
179
|
-
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${
|
|
166
|
+
summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
|
|
180
167
|
breadcrumbs: [
|
|
181
|
-
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${
|
|
168
|
+
...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${recordName} --data '{...}'`, description: `Update this ${singular}` }]),
|
|
182
169
|
{ action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
|
|
183
170
|
],
|
|
184
171
|
});
|
|
@@ -191,7 +178,8 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
191
178
|
params.set('sysparm_limit', String(limit));
|
|
192
179
|
params.set('sysparm_display_value', 'all');
|
|
193
180
|
params.set('sysparm_fields', ['sys_id', ...columns].join(','));
|
|
194
|
-
const
|
|
181
|
+
const baseQ = extraQuery ? extraQuery + '^' : '';
|
|
182
|
+
const q = query ? query + '^ORDERBYDESCsys_updated_on' : (baseQ + 'ORDERBYDESCsys_updated_on');
|
|
195
183
|
params.set('sysparm_query', q);
|
|
196
184
|
const records = await app.sdk.list(table, params);
|
|
197
185
|
app.ok({
|