@jacebenson/jsn 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,325 +1,212 @@
1
- // Catalog item management commands
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: 'catalog <subcommand>',
9
- aliases: ['cat'],
10
- describe: 'Manage Service Catalog items and variables',
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-item',
16
- describe: 'Create a catalog item (sc_cat_item) with variables',
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, describe: 'Catalog item name' })
19
- .option('short-description', { alias: 'd', type: 'string', describe: 'Short description' })
20
- .option('description', { type: 'string', describe: 'Full description' })
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
- // If --variables is provided, use submit_produce API (request against existing item)
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 catParams = new URLSearchParams();
76
- catParams.set('sysparm_query', `title=${argv.category}`);
77
- catParams.set('sysparm_limit', '1');
78
- catParams.set('sysparm_fields', 'sys_id,title');
79
- const cats = await app.sdk.list('sc_category', catParams);
80
- if (cats.length > 0) {
81
- categoryID = cats[0].sys_id?.value || cats[0].sys_id;
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: shortDesc,
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: 'items <subcommand>',
153
- aliases: ['item'],
154
- describe: 'Browse catalog items and inspect variables',
41
+ command: 'list',
42
+ aliases: ['ls'],
43
+ describe: 'List catalog items',
155
44
  builder: (y) => y
156
- .command({
157
- command: 'list',
158
- aliases: ['ls'],
159
- describe: 'List catalog items',
160
- builder: (y) => y
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
- // Separate standalone vs variable-set variables
215
- const standaloneVars = [];
216
- const setVars = new Map(); // set_sys_id -> { name, label, variables: [] }
217
- for (const v of variables) {
218
- const vs = v.variable_set?.value || v.variable_set;
219
- const entry = {
220
- sys_id: v.sys_id?.value || v.sys_id,
221
- name: v.name?.value || v.name || '',
222
- question_text: v.question_text?.display_value || v.question_text || '',
223
- type: v.type?.display_value || v.type,
224
- order: v.order?.display_value || v.order,
225
- mandatory: v.mandatory?.display_value || v.mandatory,
226
- };
227
- if (vs) {
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
- // Try to resolve variable set names
238
- for (const [sysID, setData] of setVars) {
239
- try {
240
- const vsRecord = await app.sdk.get('sc_cat_item_option_set', sysID);
241
- setData.name = vsRecord.name?.value || vsRecord.name || sysID;
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
- app.ok({
250
- sys_id: itemID,
251
- name: item.name?.display_value || item.name,
252
- short_description: item.short_description?.display_value || item.short_description || '',
253
- description: item.description?.display_value || item.description || '',
254
- category: item.category?.display_value || '',
255
- active: item.active?.display_value || item.active,
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: items.length,
81
+ count: records.length,
289
82
  columns: ['name', 'short_description', 'category', 'active'],
290
- records: items.map(r => ({
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: `${items.length} catalog item(s)` });
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: (argv) => {
303
- if (!argv._[1]) {
304
- console.log('Manage Service Catalog items.\n');
305
- console.log('Commands:');
306
- console.log(' create-item Create a catalog item, or submit a request via submit_produce');
307
- console.log(' items list List catalog items');
308
- console.log(' items show Show catalog item details with variables');
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 readline from 'node:readline';
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 in TTY with auto format and no explicit query
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
- const pickerColumns = ['sys_id', 'name', ...columns.filter(c => c !== 'name' && c !== 'sys_id')];
117
- const pickerParams = new URLSearchParams();
118
- pickerParams.set('sysparm_limit', String(limit));
119
- pickerParams.set('sysparm_display_value', 'all');
120
- pickerParams.set('sysparm_fields', pickerColumns.join(','));
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 pickerRecords = await app.sdk.list(table, pickerParams);
124
- if (pickerRecords.length === 0) {
125
- app.ok({
126
- table,
127
- count: 0,
128
- columns: pickerColumns,
129
- records: [],
130
- pagination: { limit, offset: 0 },
131
- context: { instance_url: app.getEffectiveInstance() },
132
- }, {
133
- summary: `0 ${name}(s)`,
134
- breadcrumbs: buildHints(name, singular, readOnly),
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
- return;
137
- }
141
+ };
138
142
 
139
- const choices = pickerRecords.map(r => {
140
- const recordName = getStringField(r, 'name') || getStringField(r, 'sys_id');
141
- const scope = getStringField(r, 'sys_scope') || '';
142
- let label = recordName;
143
- if (scope && scope !== 'global') label += ` [${scope}]`;
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=${selectedName}`);
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: ${selectedName}`);
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)}: ${selectedName}`,
166
+ summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
180
167
  breadcrumbs: [
181
- ...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${selectedName} --data '{...}'`, description: `Update this ${singular}` }]),
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 q = query ? query + '^ORDERBYDESCsys_updated_on' : 'ORDERBYDESCsys_updated_on';
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({