@jacebenson/jsn 3.0.6 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  // Custom scraPI command: list/show individual operations + one-shot API definition creation
2
2
  //
3
3
  // The 'create' subcommand lets you build a full Scripted REST API in one call:
4
- // jsn dev scrapi create \
4
+ // jsn scrapi create \
5
5
  // --name "SL Managed Services" --service-id managed_services --namespace slapi \
6
6
  // --resource create=POST:/create:./create.js \
7
7
  // --resource comment=POST:/comment:./comment.js
@@ -74,7 +74,7 @@ export function scrapiCmd(wrap) {
74
74
  builder: (y) => y
75
75
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident")' })
76
76
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns' })
77
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
77
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
78
78
  handler: wrap(async (argv, app) => {
79
79
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'sys_ws_definition', 'http_method', 'relative_path', 'sys_scope'];
80
80
  const query = argv.query || '';
@@ -97,8 +97,8 @@ export function scrapiCmd(wrap) {
97
97
  }, {
98
98
  summary: `${records.length} operation(s)`,
99
99
  breadcrumbs: [
100
- { action: 'create', cmd: 'jsn dev scrapi create --name "My API" --service-id my-api --namespace myapp --resource get=GET:/items:./script.js', description: 'Create a new REST API' },
101
- { action: 'show', cmd: 'jsn dev scrapi show <name>', description: 'Show operation details' },
100
+ { action: 'create', cmd: 'jsn scrapi create --name "My API" --service-id my-api --namespace myapp --resource get=GET:/items:./script.js', description: 'Create a new REST API' },
101
+ { action: 'show', cmd: 'jsn scrapi show <name>', description: 'Show operation details' },
102
102
  ],
103
103
  });
104
104
  }),
@@ -128,7 +128,7 @@ export function scrapiCmd(wrap) {
128
128
  return app.ok(records[0], {
129
129
  summary: `Operation: ${getStringField(records[0], 'name') || id}`,
130
130
  breadcrumbs: [
131
- { action: 'list', cmd: 'jsn dev scrapi list', description: 'Back to all operations' },
131
+ { action: 'list', cmd: 'jsn scrapi list', description: 'Back to all operations' },
132
132
  ],
133
133
  });
134
134
  }
@@ -182,7 +182,7 @@ export function scrapiCmd(wrap) {
182
182
  formatted += ` ${(method + ' ').slice(0, 7)} ${relPath.padEnd(30)} ${opName}${active !== 'true' ? ' (inactive)' : ''}\n`;
183
183
  }
184
184
  } else {
185
- formatted += `\n (no resources — run jsn dev scrapi create --resource ... to add some)\n`;
185
+ formatted += `\n (no resources — run jsn scrapi create --resource ... to add some)\n`;
186
186
  }
187
187
 
188
188
  app.ok({
@@ -207,8 +207,8 @@ export function scrapiCmd(wrap) {
207
207
  }, {
208
208
  summary: `API: ${defName} — ${childOps.length} resource(s)`,
209
209
  breadcrumbs: [
210
- { action: 'create', cmd: 'jsn dev scrapi create --name "..." --service-id ... --namespace ... --resource ...', description: 'Create a new API' },
211
- { action: 'list', cmd: 'jsn dev scrapi list', description: 'Back to all operations' },
210
+ { action: 'create', cmd: 'jsn scrapi create --name "..." --service-id ... --namespace ... --resource ...', description: 'Create a new API' },
211
+ { action: 'list', cmd: 'jsn scrapi list', description: 'Back to all operations' },
212
212
  ],
213
213
  });
214
214
  }),
@@ -316,8 +316,8 @@ export function scrapiCmd(wrap) {
316
316
  summary: `Created API "${apiName}" with ${operations.length} operation(s)`,
317
317
  notice: `View API: ${apiURL}`,
318
318
  breadcrumbs: [
319
- { action: 'show', cmd: `jsn dev scrapi list --query "web_service_definition=${defSysId}"`, description: 'List operations for this API' },
320
- { action: 'add', cmd: `jsn dev scrapi add-resource --api "${serviceId}" --name <name>=METHOD:/path:./script.js`, description: 'Add another resource' },
319
+ { action: 'show', cmd: `jsn scrapi list --query "web_service_definition=${defSysId}"`, description: 'List operations for this API' },
320
+ { action: 'add', cmd: `jsn scrapi add-resource --api "${serviceId}" --name <name>=METHOD:/path:./script.js`, description: 'Add another resource' },
321
321
  ],
322
322
  });
323
323
  }),
@@ -333,9 +333,9 @@ export function scrapiCmd(wrap) {
333
333
  console.log(' create Create a new API with operations (one-shot)');
334
334
  console.log('');
335
335
  console.log('Examples:');
336
- console.log(' jsn dev scrapi list');
337
- console.log(' jsn dev scrapi show my_operation');
338
- console.log(' jsn dev scrapi create \\');
336
+ console.log(' jsn scrapi list');
337
+ console.log(' jsn scrapi show my_operation');
338
+ console.log(' jsn scrapi create \\');
339
339
  console.log(' --name "My API" --service-id my-api --namespace myapp \\');
340
340
  console.log(' --resource "create=POST:/create:./create.js" \\');
341
341
  console.log(' --resource "list=GET:/list:./list.js"');
@@ -15,7 +15,7 @@ export function updateSetsCmd(wrap) {
15
15
  builder: (y) => y
16
16
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
17
17
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
18
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
18
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
19
19
  handler: wrap(async (argv, app) => {
20
20
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'state', 'application'];
21
21
  const query = argv.query || '';
@@ -24,6 +24,7 @@ export function updateSetsCmd(wrap) {
24
24
  app, table: 'sys_update_set', singular: 'update set', columns, limit: argv.limit, query, labelField: 'name',
25
25
  formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'state') || '?'}]`,
26
26
  });
27
+ if (picked === undefined) return; // user cancelled
27
28
  if (picked) {
28
29
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_update_set' };
29
30
  return app.ok(picked, { summary: `Update set: ${getStringField(picked, 'name')}` });
@@ -151,12 +152,12 @@ export function updateSetsCmd(wrap) {
151
152
  breadcrumbs: [
152
153
  {
153
154
  action: 'set',
154
- cmd: `jsn dev updatesets set "${argv.name}"`,
155
+ cmd: `jsn updatesets set "${argv.name}"`,
155
156
  description: 'Switch to this update set',
156
157
  },
157
158
  {
158
159
  action: 'complete',
159
- cmd: `jsn dev updatesets complete "${argv.name}"`,
160
+ cmd: `jsn updatesets complete "${argv.name}"`,
160
161
  description: 'Mark as complete when done',
161
162
  },
162
163
  ],
@@ -225,9 +226,9 @@ export function updateSetsCmd(wrap) {
225
226
  console.log(' export <name> Export an update set to XML');
226
227
  console.log(' complete <name> Mark an update set as complete (coming soon)');
227
228
  console.log(' yolo Silence the "Default update set" warning');
228
- console.log('\nRun "jsn dev updatesets <command> --help" for details.');
229
+ console.log('\nRun "jsn updatesets <command> --help" for details.');
229
230
  console.log('\nTip: Create an update set first:');
230
- console.log(' jsn dev updatesets create --name "My Feature"');
231
+ console.log(' jsn updatesets create --name "My Feature"');
231
232
  console.log(' # → Auto-set as current, then record changes are captured.');
232
233
  }
233
234
  },
@@ -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
  }
@@ -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
  }
@@ -74,7 +74,7 @@ export function recordsCmd(wrap) {
74
74
  const breadcrumbs = [
75
75
  { action: 'create', cmd: `jsn records create --table ${table} --data '{...}'`, description: 'Create a new record' },
76
76
  { action: 'filter', cmd: `jsn records list --table ${table} --query "priority=1"`, description: 'Filter: priority 1 only' },
77
- { action: 'columns', cmd: `jsn dev columns --table ${table}`, description: 'View available columns' },
77
+ { action: 'columns', cmd: `jsn columns --table ${table}`, description: 'View available columns' },
78
78
  ];
79
79
  if (argv['sys-id']) {
80
80
  breadcrumbs.unshift({
@@ -188,12 +188,12 @@ export function recordsCmd(wrap) {
188
188
  const breadcrumbs = [
189
189
  ...result.flows.map(f => ({
190
190
  action: 'show',
191
- cmd: `jsn dev flows show "${f.flow}"`,
191
+ cmd: `jsn flows show "${f.flow}"`,
192
192
  description: `View flow details for ${f.flow}`,
193
193
  })),
194
194
  ...result.businessRules.slice(0, 5).map(br => ({
195
195
  action: 'show',
196
- cmd: `jsn dev rules show "${br.name}"`,
196
+ cmd: `jsn rules show "${br.name}"`,
197
197
  description: `View business rule: ${br.name}`,
198
198
  })),
199
199
  ];
@@ -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
  }
@@ -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
  }
@@ -117,6 +117,17 @@ export function ticketsCmd(wrap) {
117
117
  })
118
118
 
119
119
  },
120
- handler: () => {},
120
+ handler: () => {
121
+ console.log('Query generic tickets from the ticket table.');
122
+ console.log('');
123
+ console.log('Available subcommands:');
124
+ console.log(' list List tickets');
125
+ console.log(' show <number or sys_id> Show a ticket');
126
+ console.log(' create Create a new ticket');
127
+ console.log(' update <identifier> Update a ticket');
128
+ console.log(' delete <identifier> Delete a ticket');
129
+ console.log('');
130
+ console.log('Run "jsn tickets <command> --help" for details.');
131
+ },
121
132
  };
122
133
  }
@@ -140,6 +140,17 @@ export function usersCmd(wrap) {
140
140
  }),
141
141
  });
142
142
  },
143
- handler: () => {},
143
+ handler: () => {
144
+ console.log('Manage ServiceNow users from the sys_user table.');
145
+ console.log('');
146
+ console.log('Available subcommands:');
147
+ console.log(' list List users');
148
+ console.log(' show <name or sys_id> Show a user by user_name or sys_id');
149
+ console.log(' create Create a new user');
150
+ console.log(' update <identifier> Update a user');
151
+ console.log(' delete <identifier> Delete a user');
152
+ console.log('');
153
+ console.log('Run "jsn users <command> --help" for details.');
154
+ },
144
155
  };
145
156
  }
package/src/help.js CHANGED
@@ -1,47 +1,140 @@
1
1
  // Custom grouped help renderer
2
- // Mirrors the Go version's custom Cobra help template
3
-
4
- const COMMAND_GROUPS = {
5
- 'CORE COMMANDS': [
6
- { name: 'incidents', alias: 'inc', desc: 'Manage incidents' },
7
- { name: 'changes', alias: 'chg', desc: 'Manage change requests' },
8
- { name: 'requests', alias: 'req', desc: 'Manage service catalog requests' },
9
- { name: 'tasks', alias: 'task', desc: 'Manage catalog tasks' },
10
- { name: 'tickets', alias: 'ticket', desc: 'Query generic tickets' },
11
- ],
12
- 'DATA & ADMIN': [
13
- { name: 'records', desc: 'Generic Table API for any table' },
14
- { name: 'users', alias: 'user', desc: 'Manage ServiceNow users' },
15
- { name: 'groups', alias: 'group', desc: 'Manage groups' },
16
- { name: 'groupmembers', alias: 'gm', desc: 'Manage group memberships' },
17
- { name: 'grouproles', alias: 'gr', desc: 'Manage group roles' },
18
- ],
19
- 'DEVELOPMENT': [
20
- { name: 'dev', desc: 'Manage development artifacts (flows, includes, rules, ACLs, etc.)' },
21
- ],
22
- 'CONFIGURATION': [
23
- { name: 'setup', desc: 'Interactive first-time setup' },
24
- { name: 'auth', desc: 'Manage OAuth authentication' },
25
- { name: 'profiles', alias: 'profile', desc: 'Manage instance profiles' },
26
- ],
27
- };
28
-
29
- const UTILITY_COMMANDS = [
30
- { name: 'skill', desc: 'Manage the jsn AI agent skill file (show, fetch, install)' },
31
- { name: 'version', desc: 'Show version information (--check for npm updates)' },
32
- ];
2
+ // Organized by ServiceNow domain model with subgroup dividers
33
3
 
34
4
  function renderGroup(name, commands) {
35
5
  const lines = [`\n${name}`];
36
6
  lines.push('─'.repeat(50));
37
7
  for (const cmd of commands) {
8
+ if (cmd.divider) {
9
+ lines.push('');
10
+ lines.push(` ${cmd.divider}`);
11
+ continue;
12
+ }
38
13
  const aliasPart = cmd.alias ? ` (${cmd.alias})` : '';
39
14
  const padded = ` jsn ${cmd.name}`.padEnd(22);
40
- lines.push(`${padded}${cmd.desc}${aliasPart}`);
15
+ const status = cmd.status ? ` ${cmd.status}` : '';
16
+ lines.push(`${padded}${cmd.desc}${aliasPart}${status}`);
41
17
  }
42
18
  return lines.join('\n');
43
19
  }
44
20
 
21
+ const CORE_COMMANDS = [
22
+ { name: 'incidents', alias: 'inc', desc: 'Manage incidents' },
23
+ { name: 'changes', alias: 'chg', desc: 'Manage change requests' },
24
+ { name: 'requests', alias: 'req', desc: 'Manage service catalog requests' },
25
+ { name: 'tasks', alias: 'task', desc: 'Manage catalog tasks' },
26
+ ];
27
+
28
+ const AUTOMATION_COMMANDS = [
29
+ { divider: '── Async ──' },
30
+ { name: 'flows', alias: 'flow', desc: 'Flow Designer flows' },
31
+ { name: 'actions', alias: 'action', desc: 'Flow action definitions' },
32
+ { name: 'scriptactions', desc: 'Script actions' },
33
+ { name: 'scheduledjobs', desc: 'Scheduled jobs' },
34
+ { name: 'triggers', desc: 'Event triggers' },
35
+ { name: 'asyncrules', desc: 'Asynchronous business rules' },
36
+
37
+ { divider: '── In Memory ──' },
38
+ { name: 'rules', alias: 'br', desc: 'Business rules (create, update, delete)' },
39
+ { name: 'workflows', desc: 'Workflow definitions' },
40
+ { name: 'decisiontables', desc: 'Decision tables' },
41
+ { name: 'assignments', desc: 'Assignment rules' },
42
+
43
+ { divider: '── Inbound ──' },
44
+ { name: 'scrapi', desc: 'Scripted REST APIs' },
45
+ { name: 'email', desc: 'Inbound email actions' },
46
+
47
+ { divider: '── Outbound ──' },
48
+ { name: 'restmessage', alias: 'rm', desc: 'REST Messages (sys_rest_message)' },
49
+ { name: 'restmethods', desc: 'REST Message methods (sys_rest_message_fn)' },
50
+ { name: 'soapmessages', desc: 'SOAP Messages' },
51
+ { name: 'soapfunctions', desc: 'SOAP Message functions' },
52
+ ];
53
+
54
+ const ACCESS_COMMANDS = [
55
+ { name: 'acls', alias: 'acl', desc: 'Access controls' },
56
+ { name: 'b4rules', alias: 'b4r', desc: 'Before-query business rules (see also Automation)' },
57
+ { name: 'roles', alias: 'role', desc: 'Roles' },
58
+ { name: 'privileges', alias: 'priv', desc: 'Scope privileges' },
59
+ { name: 'securitytypes', alias: 'st', desc: 'Custom security types' },
60
+ { name: 'aliases', alias: 'als', desc: 'Connection & credential aliases' },
61
+ { name: 'groups', alias: 'group', desc: 'Manage groups' },
62
+ { name: 'groupmembers', alias: 'gm', desc: 'Group memberships' },
63
+ { name: 'grouproles', alias: 'gr', desc: 'Group roles' },
64
+ { name: 'users', alias: 'user', desc: 'Manage users' },
65
+ ];
66
+
67
+ const UX_COMMANDS = [
68
+ { divider: '── Shared (across all experiences) ──' },
69
+ { name: 'forms', desc: 'Form layouts' },
70
+ { name: 'lists', desc: 'List layouts' },
71
+ { name: 'clientscripts', alias: 'cs', desc: 'Client scripts' },
72
+ { name: 'uipolicies', alias: 'up', desc: 'UI policies' },
73
+ { name: 'uiactions', alias: 'ua', desc: 'UI actions (buttons, links, context menus)' },
74
+ { name: 'views', alias: 'vw', desc: 'Views' },
75
+ { name: 'catalogitems', alias: 'ci', desc: 'Service Catalog items + variables' },
76
+ { name: 'catalogscripts', desc: 'Catalog client scripts' },
77
+ { name: 'cataloguipolicies', alias: 'cup', desc: 'Catalog UI policies' },
78
+
79
+ { divider: '── Core UI ──' },
80
+ { name: 'uipages', desc: 'UI pages' },
81
+ { name: 'uimacros', desc: 'UI macros' },
82
+ { name: 'listcontrols', alias: 'lc', desc: 'List controls' },
83
+
84
+ { divider: '── Service Portal ──' },
85
+ { name: 'sppages', desc: 'Service Portal pages' },
86
+ { name: 'spwidgets', desc: 'Service Portal widgets' },
87
+
88
+ { divider: '── Next Experience (Workspaces) ──' },
89
+ { name: 'uxscripts', alias: 'ux', desc: 'UX source scripts' },
90
+ { name: 'uxlists', desc: 'Workspace lists (sys_ux_list)' },
91
+ { name: 'uxapplicability', desc: 'Workspace applicability (m2m)' },
92
+
93
+ { divider: '── Navigation ──' },
94
+ { name: 'appmenu', desc: 'Application menus' },
95
+ { name: 'appmodules', alias: 'am', desc: 'Application modules' },
96
+ ];
97
+
98
+ const DATA_COMMANDS = [
99
+ { divider: '── DB Schema ──' },
100
+ { name: 'tables', alias: 't', desc: 'Table definitions' },
101
+ { name: 'columns', alias: 'col', desc: 'Column definitions' },
102
+ { name: 'relationships', alias: 'rel', desc: 'Table relationships' },
103
+
104
+ { divider: '── Shared Code, Transforms, Logs ──' },
105
+ { name: 'includes', alias: 'si', desc: 'Script includes' },
106
+ { name: 'import', desc: 'Import sets' },
107
+ { name: 'logs', desc: 'System logs' },
108
+ { name: 'properties', alias: 'prop', desc: 'System properties' },
109
+
110
+ { name: 'records', desc: 'Generic Table API (any table)' },
111
+ ];
112
+
113
+ const CONFIG_COMMANDS = [
114
+ { name: 'setup', desc: 'Interactive first-time setup' },
115
+ { name: 'auth', desc: 'OAuth authentication' },
116
+ { name: 'profiles', alias: 'profile', desc: 'Instance profiles' },
117
+ { name: 'updatesets', desc: 'Update sets (set, yolo, export, import)' },
118
+ { name: 'scopes', desc: 'Application scopes' },
119
+ ];
120
+
121
+ const DEVELOPER_COMMANDS = [
122
+ { name: 'eval', desc: 'Run server-side JavaScript' },
123
+ { name: 'rest', desc: 'Raw REST API calls' },
124
+ { name: 'skill', desc: 'AI agent skill file (show, fetch, install)' },
125
+ { name: 'version', desc: 'Version info (--check for npm updates)' },
126
+ ];
127
+
128
+ const COMMAND_GROUPS = {
129
+ 'CORE': CORE_COMMANDS,
130
+ 'AUTOMATION': AUTOMATION_COMMANDS,
131
+ 'ACCESS': ACCESS_COMMANDS,
132
+ 'USER EXPERIENCE': UX_COMMANDS,
133
+ 'DATA': DATA_COMMANDS,
134
+ 'CONFIGURATION': CONFIG_COMMANDS,
135
+ 'DEVELOPER': DEVELOPER_COMMANDS,
136
+ };
137
+
45
138
  function renderFlags() {
46
139
  return `
47
140
  FLAGS
@@ -67,7 +160,6 @@ LEARN MORE
67
160
 
68
161
  export function renderHelp() {
69
162
  const sections = Object.entries(COMMAND_GROUPS).map(([name, cmds]) => renderGroup(name, cmds));
70
- const utilitySection = renderGroup('UTILITY', UTILITY_COMMANDS);
71
163
 
72
164
  return [
73
165
  'Usage: jsn <command> [options]',
@@ -76,8 +168,6 @@ export function renderHelp() {
76
168
  '',
77
169
  ...sections,
78
170
  '',
79
- utilitySection,
80
- '',
81
171
  renderFlags(),
82
172
  renderTips(),
83
173
  '',
package/src/helpers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Shared helper utilities
2
2
 
3
3
  import fs from 'node:fs';
4
- import search from '@inquirer/search';
4
+ import { paginatedSearch } from './paginated-search.js';
5
5
  import { isTTY, FormatAuto } from './output.js';
6
6
 
7
7
  export function getStringField(record, field) {
@@ -89,53 +89,69 @@ export function resolveFieldsParam(columns) {
89
89
  * @param {string} opts.labelField — field used to match selection (default: 'name')
90
90
  * @returns {Promise<void>|null} null if no selection made or non-interactive
91
91
  */
92
- export async function interactiveList({ app, table, singular, columns, limit = 20, query = '', formatLabel, labelField = 'name' }) {
92
+ function vowelArticle(word) {
93
+ const first = word.charAt(0).toLowerCase();
94
+ return first === 'a' || first === 'e' || first === 'i' || first === 'o' || first === 'u' ? 'an' : 'a';
95
+ }
96
+
97
+ export async function interactiveList({ app, table, singular, columns, limit = 50, query = '', formatLabel, labelField = 'name' }) {
98
+ app.requireInstance();
93
99
  const effectiveFormat = app.output.getFormat() === FormatAuto ? (isTTY(process.stdout) ? FormatAuto : FormatAuto) : app.output.getFormat();
94
- if (effectiveFormat !== FormatAuto || !isTTY(process.stdout) || !isTTY(process.stdin) || query) {
100
+ if (effectiveFormat !== FormatAuto || !isTTY(process.stdout) || !isTTY(process.stdin)) {
95
101
  return null; // not interactive — caller should fall back to text/table
96
102
  }
97
103
 
98
104
  const pickerColumns = ['sys_id', labelField, ...columns.filter(c => c !== labelField && c !== 'sys_id' && c !== '*')];
99
- const params = new URLSearchParams();
100
- params.set('sysparm_limit', String(limit));
101
- params.set('sysparm_display_value', 'all');
102
105
  const pickerFields = pickerColumns.join(',');
103
- if (pickerFields) params.set('sysparm_fields', pickerFields);
104
- params.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
105
-
106
- const records = await app.sdk.list(table, params);
107
- if (records.length === 0) return null;
108
-
109
- const choices = records.map(r => ({
110
- name: formatLabel ? formatLabel(r) : (getStringField(r, labelField) || getStringField(r, 'sys_id')),
111
- value: r,
112
- }));
113
106
 
107
+ // Get total count
108
+ let totalCount = 0;
114
109
  try {
115
- const selected = await search({
116
- message: `Select ${vowelArticle(singular)} ${singular}:`,
117
- source: async (input) => {
118
- if (!input) return choices;
119
- const term = input.toLowerCase();
120
- return choices.filter(c => c.name.toLowerCase().includes(term));
121
- },
122
- });
123
- return selected; // the record object
124
- } catch (err) {
125
- if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
126
- return null;
110
+ totalCount = await app.sdk.aggregateCount(table, '');
111
+ } catch {
112
+ totalCount = 0;
113
+ }
114
+ if (totalCount === 0) return null;
115
+
116
+ // Paginated source adapter: (term, offset, { signal }) => choices[]
117
+ async function serverSource(term, offset, { signal } = {}) {
118
+ try {
119
+ const params = new URLSearchParams();
120
+ params.set('sysparm_limit', String(limit));
121
+ params.set('sysparm_display_value', 'all');
122
+ if (pickerFields) params.set('sysparm_fields', pickerFields);
123
+ if (term) {
124
+ params.set('sysparm_query', `${labelField}LIKE${term}^ORDERBYDESCsys_updated_on`);
125
+ } else {
126
+ params.set('sysparm_query', 'ORDERBYDESCsys_updated_on');
127
+ params.set('sysparm_offset', String(offset));
128
+ }
129
+ const records = await app.sdk.list(table, params);
130
+ if (!Array.isArray(records)) {
131
+ console.error('SDK non-array:', typeof records);
132
+ return [];
133
+ }
134
+ return records.map(r => ({
135
+ name: formatLabel ? formatLabel(r) : (getStringField(r, labelField) || getStringField(r, 'sys_id')),
136
+ value: r,
137
+ }));
138
+ } catch (err) {
139
+ console.error('serverSource error:', err.message || err);
140
+ return [];
127
141
  }
128
- throw err;
129
142
  }
130
- }
131
143
 
132
- function vowelArticle(word) {
133
- const first = word.charAt(0).toLowerCase();
134
- return first === 'a' || first === 'e' || first === 'i' || first === 'o' || first === 'u' ? 'an' : 'a';
144
+ const selected = await paginatedSearch({
145
+ message: `Select ${singular === 'flow' ? 'a flow' : `a ${singular}`}`,
146
+ pageSize: 10,
147
+ totalCount,
148
+ source: serverSource,
149
+ });
150
+
151
+ return selected?.value; // unwrap {name, value} → raw record, undefined on cancel
135
152
  }
136
153
 
137
154
  /**
138
- * Known derived (read-only) ServiceNow fields that should not be set directly.
139
155
  * Maps table name → array of field names that are computed by the platform.
140
156
  * When a create/update payload contains these, a warning is emitted.
141
157
  */