@jacebenson/jsn 3.1.2 → 3.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -38,9 +38,9 @@ import {
38
38
  privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
39
39
  catalogscriptsCmd, cataloguipoliciesCmd,
40
40
  scriptactionsCmd, scheduledjobsCmd, asyncrulesCmd, triggersCmd,
41
- workflowsCmd, decisiontablesCmd, assignmentsCmd,
41
+ decisiontablesCmd, assignmentsCmd,
42
42
  emailCmd,
43
- restmessageCmd, restmethodsCmd, soapmessagesCmd, soapfunctionsCmd,
43
+ restmessageCmd, soapmessagesCmd,
44
44
  uimacrosCmd, uxlistsCmd, uxapplicabilityCmd,
45
45
  } from './commands/dev/_simple.js';
46
46
  import { flowsCmd } from './commands/dev/flows.js';
@@ -222,7 +222,6 @@ export function buildCLI() {
222
222
  .command(triggersCmd(wrap))
223
223
  // AUTOMATION — In Memory
224
224
  .command(rulesCmd(wrap))
225
- .command(workflowsCmd(wrap))
226
225
  .command(decisiontablesCmd(wrap))
227
226
  .command(assignmentsCmd(wrap))
228
227
  // AUTOMATION — Inbound
@@ -230,9 +229,7 @@ export function buildCLI() {
230
229
  .command(emailCmd(wrap))
231
230
  // AUTOMATION — Outbound
232
231
  .command(restmessageCmd(wrap))
233
- .command(restmethodsCmd(wrap))
234
232
  .command(soapmessagesCmd(wrap))
235
- .command(soapfunctionsCmd(wrap))
236
233
  // ACCESS
237
234
  .command(aclsCmd(wrap))
238
235
  .command(b4rulesCmd(wrap))
@@ -14,9 +14,34 @@ export function catalogCmd(wrap) {
14
14
  builder: (y) => y
15
15
  .option('name', { alias: 'n', type: 'string', demandOption: true })
16
16
  .option('short-description', { alias: 'd', type: 'string' })
17
- .option('category', { alias: 'c', type: 'string' }),
17
+ .option('category', { alias: 'c', type: 'string' })
18
+ .option('variable', { alias: 'v', type: 'array', describe: 'Variable: "name:type:label"' })
19
+ .option('variables', { type: 'string', describe: 'JSON for submit_produce: {\"key\":\"value\"}' }),
18
20
  handler: wrap(async (argv, app) => {
19
21
  app.requireInstance();
22
+
23
+ // submit_produce path
24
+ if (argv.variables) {
25
+ let varsObj;
26
+ try { varsObj = JSON.parse(argv.variables); } catch { throw new Error('--variables must be valid JSON'); }
27
+ let itemSysID = argv.name;
28
+ if (!/^[a-f0-9]{32}$/i.test(itemSysID)) {
29
+ const sp = new URLSearchParams();
30
+ sp.set('sysparm_query', `name=${itemSysID}`);
31
+ sp.set('sysparm_limit', '1');
32
+ sp.set('sysparm_fields', 'sys_id');
33
+ const items = await app.sdk.list('sc_cat_item', sp);
34
+ if (items.length === 0) throw new Error(`Not found: ${itemSysID}`);
35
+ itemSysID = items[0].sys_id?.value || items[0].sys_id;
36
+ }
37
+ const endpoint = `${app.sdk.baseURL}/api/sn_sc/servicecatalog/items/${itemSysID}/submit_produce`;
38
+ const result = await app.sdk.request(endpoint, { method: 'POST', body: JSON.stringify({ variables: varsObj }) });
39
+ const reqID = result?.result?.sys_id || result?.result?.number || '';
40
+ app.ok({ requested_item: reqID, item_id: itemSysID, variables: varsObj },
41
+ { summary: `Request: ${result?.result?.number || reqID}` });
42
+ return;
43
+ }
44
+
20
45
  let categoryID = '';
21
46
  if (argv.category) {
22
47
  const cp = new URLSearchParams();
@@ -34,7 +59,31 @@ export function catalogCmd(wrap) {
34
59
  active: true,
35
60
  type: 'item',
36
61
  });
37
- app.ok(item, { summary: `Created: ${argv.name}` });
62
+ const itemID = item.sys_id?.value || item.sys_id;
63
+
64
+ // Create variables if --variable flag(s) provided
65
+ let varCount = 0;
66
+ if (argv.variable) {
67
+ for (const v of argv.variable) {
68
+ const parts = String(v).split(':');
69
+ if (parts.length >= 2) {
70
+ const typeName = parts.length >= 2 ? parts[parts.length - 1] : '';
71
+ const label = parts.length >= 3 ? parts[parts.length - 1] : parts[0];
72
+ const sysName = parts[0].replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
73
+ try {
74
+ const typeMap = { string: '6', multilinetext: '2', select: '3', date: '4', datetime: '5', reference: '8', checkbox: '12', email: '11' };
75
+ await app.sdk.create('item_option_new', {
76
+ name: sysName, question_text: label, type: typeMap[typeName] || '6',
77
+ order: (varCount + 1) * 100, cat_item: itemID, mandatory: true, active: true,
78
+ });
79
+ varCount++;
80
+ } catch { /* non-fatal */ }
81
+ }
82
+ }
83
+ }
84
+
85
+ app.ok({ sys_id: itemID, name: argv.name, variables_created: varCount },
86
+ { summary: `Created: ${argv.name}${varCount ? ` (${varCount} variable(s))` : ''}` });
38
87
  }),
39
88
  })
40
89
  .command({
@@ -43,10 +92,11 @@ export function catalogCmd(wrap) {
43
92
  describe: 'List catalog items',
44
93
  builder: (y) => y
45
94
  .option('limit', { alias: 'l', type: 'number', default: 50 })
46
- .option('category', { type: 'string' }),
95
+ .option('category', { type: 'string' })
96
+ .option('query', { type: 'string', describe: 'Encoded query (e.g. active=true)' }),
47
97
  handler: wrap(async (argv, app) => {
48
98
  app.requireInstance();
49
- const query = argv.category ? `category.titleLIKE${argv.category}` : '';
99
+ const query = argv.query || (argv.category ? `category.titleLIKE${argv.category}` : '');
50
100
 
51
101
  const picked = await interactiveList({
52
102
  app,
@@ -69,20 +119,27 @@ export function catalogCmd(wrap) {
69
119
  return;
70
120
  }
71
121
 
72
- // Fallback — non-TTY
122
+ // Fallback — non-TTY / --json / --styled
73
123
  const p = new URLSearchParams();
74
124
  p.set('sysparm_query', query ? `${query}^ORDERBYname` : 'ORDERBYname');
75
125
  p.set('sysparm_limit', String(argv.limit));
76
126
  p.set('sysparm_display_value', 'all');
77
127
  p.set('sysparm_fields', 'name,short_description,category,active');
78
128
  const records = await app.sdk.list('sc_cat_item', p);
129
+ // Map reference fields to strings for styled/table renderer
130
+ const safe = records.map(r => ({
131
+ name: getStringField(r, 'name'),
132
+ short_description: getStringField(r, 'short_description'),
133
+ category: getStringField(r, 'category'),
134
+ active: getStringField(r, 'active'),
135
+ }));
79
136
  app.ok({
80
137
  table: 'sc_cat_item',
81
- count: records.length,
138
+ count: safe.length,
82
139
  columns: ['name', 'short_description', 'category', 'active'],
83
- records,
140
+ records: safe,
84
141
  context: { instance_url: app.getEffectiveInstance() },
85
- }, { summary: `${records.length} catalog item(s)` });
142
+ }, { summary: `${safe.length} catalog item(s)` });
86
143
  }),
87
144
  })
88
145
  .command({
@@ -101,7 +158,6 @@ export function catalogCmd(wrap) {
101
158
  if (results.length === 0) throw new Error(`Not found: ${id}`);
102
159
  id = results[0].sys_id?.value || results[0].sys_id;
103
160
  }
104
- const item = await app.sdk.get('sc_cat_item', id);
105
161
  await showItem(app, id);
106
162
  }),
107
163
  });
@@ -120,7 +176,6 @@ export function catalogCmd(wrap) {
120
176
  async function showItem(app, sysID) {
121
177
  const item = await app.sdk.get('sc_cat_item', sysID);
122
178
 
123
- // Fetch variables
124
179
  const vp = new URLSearchParams();
125
180
  vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
126
181
  vp.set('sysparm_limit', '100');
@@ -145,68 +200,40 @@ async function showItem(app, sysID) {
145
200
  standalone.push(entry);
146
201
  }
147
202
  }
148
-
149
203
  const totalVars = standalone.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0);
150
204
 
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
- }
205
+ // Resolve flow name
206
+ let flowName = '', flowCmd = '';
207
+ const flowVal = item.flow_designer_flow;
208
+ if (flowVal?.value) {
209
+ const fn = getStringField(item, 'flow_designer_flow');
210
+ if (fn === flowVal.value) {
211
+ try { const fr = await app.sdk.get('sys_hub_flow', flowVal.value); flowName = getStringField(fr, 'name') || ''; } catch { flowName = fn; }
212
+ } else { flowName = fn; }
213
+ flowCmd = `jsn flows show ${flowVal.value}`;
209
214
  }
215
+ const wfName = getStringField(item, 'workflow') || '';
216
+ const wfID = item.workflow?.value || '';
217
+ const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan') || '';
218
+ const planID = item.delivery_plan?.value || item.execution_plan?.value || '';
210
219
 
211
- console.log(lines.join('\n'));
220
+ app.ok({
221
+ name: getStringField(item, 'name'),
222
+ short_description: getStringField(item, 'short_description'),
223
+ category: getStringField(item, 'category'),
224
+ active: getStringField(item, 'active'),
225
+ url: `${app.getEffectiveInstance()}/sc_cat_item.do?sys_id=${sysID}`,
226
+ flow: flowName || undefined,
227
+ flow_cmd: flowCmd || undefined,
228
+ workflow: wfName || undefined,
229
+ workflow_cmd: (wfName && wfID) ? `jsn workflows show ${wfID}` : undefined,
230
+ execution_plan: planName || undefined,
231
+ variables: totalVars > 0 ? {
232
+ count: totalVars,
233
+ standalone: standalone.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })),
234
+ sets: Array.from(setVars.values()).map(s => ({ name: s.name, variables: s.variables.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })) })),
235
+ } : undefined,
236
+ }, {
237
+ summary: `${getStringField(item, 'name')} — ${totalVars} variable(s)`,
238
+ });
212
239
  }
@@ -81,6 +81,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
81
81
  const readOnly = opts.readOnly || false;
82
82
  const scopeValidation = opts.scopeValidation || false;
83
83
  const extraQuery = opts.extraQuery || '';
84
+ const formatLabel = opts.formatLabel || null;
84
85
  const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
85
86
  const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
86
87
  const crumbs = [
@@ -101,22 +101,20 @@ export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalo
101
101
  // ── Automation: Async ──
102
102
  export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
103
103
  export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
104
- export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'order>=1000' });
104
+ 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' });
105
105
  export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
106
106
 
107
107
  // ── Automation: In Memory ──
108
- export const workflowsCmd = (wrap) => buildDevCmd('workflows', 'wf_workflow', ['workflow'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'workflow', scopeValidation: true, readOnly: true });
109
- export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_decision', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
108
+ export const decisiontablesCmd = (wrap) => buildDevCmd('decisiontables', 'sys_ws_definition', ['decisiontable'], ['name', 'active', 'sys_scope'], wrap, { singular: 'decision table', scopeValidation: true, readOnly: true });
110
109
  export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
111
110
 
112
111
  // ── Automation: Inbound ──
113
- export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_email_action', ['emails'], ['name', 'active', 'event_name', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
112
+ export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_in_email_action', ['emails'], ['name', 'active', 'type', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
114
113
 
115
114
  // ── Automation: Outbound ──
116
- export const restmessageCmd = (wrap) => buildDevCmd('restmessage', 'sys_rest_message', ['rm', 'restmsg'], ['name', 'endpoint', 'active', 'sys_scope'], wrap, { singular: 'REST message', scopeValidation: true });
115
+ export { restmessageCmd } from './restmessage.js';
117
116
  export const restmethodsCmd = (wrap) => buildDevCmd('restmethods', 'sys_rest_message_fn', ['restmethod'], ['name', 'http_method', 'active', 'sys_scope'], wrap, { singular: 'REST method', scopeValidation: true });
118
- export const soapmessagesCmd = (wrap) => buildDevCmd('soapmessages', 'sys_soap_message', ['soapmsg'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP message', scopeValidation: true });
119
- export const soapfunctionsCmd = (wrap) => buildDevCmd('soapfunctions', 'sys_soap_message_function', ['soapfunc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'SOAP function', scopeValidation: true });
117
+ export { soapmessagesCmd } from './soapmessages.js';
120
118
 
121
119
  // ── UX: Core UI ──
122
120
  export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
@@ -0,0 +1,32 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { getStringField } from '../../helpers.js';
3
+
4
+ export const restmessageCmd = (wrap) => buildDevCmd('restmessage', 'sys_rest_message', ['rm', 'restmsg'],
5
+ ['name', 'endpoint', 'active', 'sys_scope'], wrap, {
6
+ singular: 'REST message',
7
+ scopeValidation: true,
8
+ onShow: async (record, app) => {
9
+ const sysID = getStringField(record, 'sys_id') || '';
10
+ if (!sysID) return;
11
+
12
+ // Fetch related methods
13
+ const mp = new URLSearchParams();
14
+ mp.set('sysparm_query', `rest_message=${sysID}^ORDERBYorder`);
15
+ mp.set('sysparm_limit', '100');
16
+ mp.set('sysparm_display_value', 'all');
17
+ mp.set('sysparm_fields', 'name,http_method,rest_endpoint,order,sys_id');
18
+ const methods = await app.sdk.list('sys_rest_message_fn', mp);
19
+
20
+ if (methods.length > 0) {
21
+ console.log(` Methods (${methods.length}):`);
22
+ for (const m of methods) {
23
+ const name = getStringField(m, 'name') || getStringField(m, 'http_method') || '';
24
+ const method = getStringField(m, 'http_method') || '';
25
+ const path = getStringField(m, 'rest_endpoint') || '';
26
+ const mid = getStringField(m, 'sys_id') || '';
27
+ console.log(` ${method} ${path} (${name})`);
28
+ console.log(` → jsn records get --table sys_rest_message_fn --sys-id ${mid}`);
29
+ }
30
+ }
31
+ },
32
+ });
@@ -0,0 +1,30 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { getStringField } from '../../helpers.js';
3
+
4
+ export const soapmessagesCmd = (wrap) => buildDevCmd('soapmessages', 'sys_soap_message', ['soapmsg'],
5
+ ['name', 'active', 'sys_scope'], wrap, {
6
+ singular: 'SOAP message',
7
+ scopeValidation: true,
8
+ onShow: async (record, app) => {
9
+ const sysID = getStringField(record, 'sys_id') || '';
10
+ if (!sysID) return;
11
+
12
+ // Fetch related functions
13
+ const mp = new URLSearchParams();
14
+ mp.set('sysparm_query', `soap_message=${sysID}^ORDERBYorder`);
15
+ mp.set('sysparm_limit', '100');
16
+ mp.set('sysparm_display_value', 'all');
17
+ mp.set('sysparm_fields', 'name,order,sys_id');
18
+ const funcs = await app.sdk.list('sys_soap_message_function', mp);
19
+
20
+ if (funcs.length > 0) {
21
+ console.log(` Functions (${funcs.length}):`);
22
+ for (const f of funcs) {
23
+ const name = getStringField(f, 'name') || '';
24
+ const fid = getStringField(f, 'sys_id') || '';
25
+ console.log(` ${name}`);
26
+ console.log(` → jsn records get --table sys_soap_message_function --sys-id ${fid}`);
27
+ }
28
+ }
29
+ },
30
+ });
package/src/help.js CHANGED
@@ -36,7 +36,6 @@ const AUTOMATION_COMMANDS = [
36
36
 
37
37
  { divider: '── In Memory ──' },
38
38
  { name: 'rules', alias: 'br', desc: 'Business rules (create, update, delete)' },
39
- { name: 'workflows', desc: 'Workflow definitions' },
40
39
  { name: 'decisiontables', desc: 'Decision tables' },
41
40
  { name: 'assignments', desc: 'Assignment rules' },
42
41
 
@@ -46,9 +45,7 @@ const AUTOMATION_COMMANDS = [
46
45
 
47
46
  { divider: '── Outbound ──' },
48
47
  { name: 'restmessage', alias: 'rm', desc: 'REST Messages (sys_rest_message)' },
49
- { name: 'restmethods', desc: 'REST Message methods (sys_rest_message_fn)' },
50
48
  { name: 'soapmessages', desc: 'SOAP Messages' },
51
- { name: 'soapfunctions', desc: 'SOAP Message functions' },
52
49
  ];
53
50
 
54
51
  const ACCESS_COMMANDS = [
package/src/output.js CHANGED
@@ -281,7 +281,7 @@ export class OutputWriter {
281
281
  const width = colWidths[col] || 20;
282
282
 
283
283
  if (j === 0 && instanceURL && table && row.sys_id) {
284
- const url = `${instanceURL}/${table}.do?sys_id=${row.sys_id}`;
284
+ const url = getTableURL(instanceURL, table, row.sys_id);
285
285
  val = hyperlink(val, url);
286
286
  }
287
287
 
@@ -291,7 +291,7 @@ export class OutputWriter {
291
291
  display = visible.slice(0, width - 3) + '...';
292
292
  if (val !== visible) {
293
293
  if (j === 0 && instanceURL && table && row.sys_id) {
294
- const url = `${instanceURL}/${table}.do?sys_id=${row.sys_id}`;
294
+ const url = getTableURL(instanceURL, table, row.sys_id);
295
295
  display = hyperlink(display, url);
296
296
  }
297
297
  }
@@ -417,7 +417,7 @@ export class OutputWriter {
417
417
  const sysID = getDisplayValue(data.sys_id);
418
418
  if (sysID) {
419
419
  const urlTable = table.toLowerCase().replace(/\s+/g, '_');
420
- const recordURL = `${instanceURL}/${urlTable}.do?sys_id=${sysID}`;
420
+ const recordURL = getTableURL(instanceURL, urlTable, sysID);
421
421
  this.writer.write(`Link: ${recordURL}\n\n`);
422
422
  }
423
423
  }
@@ -453,6 +453,18 @@ function detectRecord(data) {
453
453
  return { isRecord: hasSysID && (hasSysClass || tableName), tableName };
454
454
  }
455
455
 
456
+ export function getTableURL(instance, table, sysID) {
457
+ const mapped = {
458
+ sys_decision: `now/workflow-studio/builder?table=sys_decision&sysId=${sysID}`,
459
+ sys_rest_message: `nav_to.do?uri=sys_rest_message.do?sys_id=${sysID}`,
460
+ sysevent_email_action: `nav_to.do?uri=sysevent_email_action.do?sys_id=${sysID}`,
461
+ sysevent_in_email_action: `nav_to.do?uri=sysevent_in_email_action.do?sys_id=${sysID}`,
462
+ soap_message: `nav_to.do?uri=soap_message.do?sys_id=${sysID}`,
463
+ };
464
+ const path = mapped[table] || `${table}.do?sys_id=${sysID}`;
465
+ return `${instance}/${path}`;
466
+ }
467
+
456
468
  export function getDisplayValue(val) {
457
469
  if (val == null) return '';
458
470
  if (typeof val === 'string') return val;
@@ -4,16 +4,10 @@
4
4
 
5
5
  import { createPrompt, useState, useKeypress, usePagination, useEffect, useRef } from '@inquirer/core';
6
6
  import { isEnterKey, isUpKey, isDownKey } from '@inquirer/core';
7
- import { appendFileSync } from 'fs';
8
7
 
9
8
  export const paginatedSearch = createPrompt((config, done) => {
10
9
  const { message, pageSize = 10, totalCount = 0, source } = config;
11
10
 
12
- // ── DEBUG ──
13
- const DBG = '/home/jace/workspace/holly/sn-jsn-fork/debug.log';
14
- const log = (msg) => appendFileSync(DBG, `${new Date().toISOString()} ${msg}\n`);
15
- log(`START totalCount=${totalCount}`);
16
-
17
11
  const [status, setStatus] = useState('loading');
18
12
  const [searchTerm, setSearchTerm] = useState('');
19
13
  const [choices, setChoices] = useState([]);
@@ -36,12 +30,10 @@ export const paginatedSearch = createPrompt((config, done) => {
36
30
  useEffect(() => {
37
31
  const controller = new AbortController();
38
32
  setStatus('loading');
39
- log(`EFFECT-INIT searchTerm="${searchTerm}"`);
40
33
 
41
34
  const load = async () => {
42
35
  try {
43
36
  const items = await source(searchTerm || undefined, 0, { signal: controller.signal });
44
- log(`INIT-LOAD got ${items.length} items`);
45
37
  if (!controller.signal.aborted) {
46
38
  choicesRef.current = items;
47
39
  loadedRef.current = items.length;
@@ -52,7 +44,6 @@ export const paginatedSearch = createPrompt((config, done) => {
52
44
  setStatus('done');
53
45
  }
54
46
  } catch (err) {
55
- log(`INIT-LOAD error: ${err.message}`);
56
47
  if (!controller.signal.aborted) {
57
48
  setStatus('done');
58
49
  setChoices([]);
@@ -66,16 +57,10 @@ export const paginatedSearch = createPrompt((config, done) => {
66
57
 
67
58
  // Load more when scrolling past end
68
59
  async function loadMore() {
69
- log(`loadMore check: loaded=${loadedRef.current} total=${totalCountRef.current} searchMode=${searchModeRef.current}`);
70
- if (loadedRef.current >= totalCountRef.current || searchModeRef.current) {
71
- log(`loadMore SKIPPED`);
72
- return;
73
- }
60
+ if (loadedRef.current >= totalCountRef.current || searchModeRef.current) return;
74
61
  setStatus('loading');
75
- log(`loadMore calling source offset=${loadedRef.current}`);
76
62
  try {
77
63
  const newItems = await source('', loadedRef.current, { signal: new AbortController().signal });
78
- log(`loadMore got ${newItems.length} items`);
79
64
  const updated = choicesRef.current.concat(newItems);
80
65
  choicesRef.current = updated;
81
66
  loadedRef.current = updated.length;
@@ -166,7 +151,6 @@ export const paginatedSearch = createPrompt((config, done) => {
166
151
  activeRef.current++;
167
152
  setActive(activeRef.current);
168
153
  } else if (!searchModeRef.current && loadedRef.current < totalCountRef.current) {
169
- log(`DOWN at end: active=${activeRef.current} len=${choicesRef.current.length} loaded=${loadedRef.current} total=${totalCountRef.current}`);
170
154
  loadMore().then(() => {
171
155
  activeRef.current++;
172
156
  setActive(activeRef.current);
package/src/sdk.js CHANGED
@@ -139,10 +139,6 @@ export class SDKClient {
139
139
  async list(table, params = {}) {
140
140
  const query = new URLSearchParams(params).toString();
141
141
  const endpoint = `${this.baseURL}/api/now/table/${table}${query ? '?' + query : ''}`;
142
- // Debug: log paginated queries
143
- if (params.sysparm_offset) {
144
- process.stderr.write(`[SDK list] offset=${params.sysparm_offset} limit=${params.sysparm_limit} table=${table}\n`);
145
- }
146
142
  const result = await this.request(endpoint, { method: 'GET' });
147
143
  return result?.result || [];
148
144
  }