@jacebenson/jsn 4.1.0 → 4.2.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.
Files changed (47) hide show
  1. package/README.md +18 -2
  2. package/package.json +4 -1
  3. package/src/app.js +49 -65
  4. package/src/auth.js +46 -11
  5. package/src/capabilities.js +128 -0
  6. package/src/cli.js +129 -79
  7. package/src/commands/_ticket.js +36 -92
  8. package/src/commands/approvals.js +350 -0
  9. package/src/commands/atf.js +635 -0
  10. package/src/commands/auth.js +93 -13
  11. package/src/commands/catalog/enrich.js +139 -0
  12. package/src/commands/catalog.js +15 -133
  13. package/src/commands/cmdb.js +613 -0
  14. package/src/commands/codesearch.js +103 -0
  15. package/src/commands/dev/_generic.js +62 -110
  16. package/src/commands/dev/_simple.js +16 -12
  17. package/src/commands/dev/b4rules.js +1 -1
  18. package/src/commands/dev/domains.js +4 -0
  19. package/src/commands/dev/eval.js +40 -4
  20. package/src/commands/dev/flows.js +3 -0
  21. package/src/commands/dev/logs.js +97 -0
  22. package/src/commands/dev/rest.js +6 -0
  23. package/src/commands/dev/restmessage.js +1 -0
  24. package/src/commands/dev/scopes.js +49 -25
  25. package/src/commands/dev/scrapi.js +5 -0
  26. package/src/commands/dev/soapmessages.js +1 -0
  27. package/src/commands/dev/updatesets.js +145 -15
  28. package/src/commands/dev.js +3 -2
  29. package/src/commands/docs/db.js +14 -0
  30. package/src/commands/docs/docs.js +9 -21
  31. package/src/commands/docs/search.js +33 -10
  32. package/src/commands/groups.js +3 -0
  33. package/src/commands/records.js +254 -9
  34. package/src/commands/setup.js +5 -0
  35. package/src/commands/skill.js +3 -0
  36. package/src/commands/snippets.js +155 -0
  37. package/src/commands/tickets.js +9 -29
  38. package/src/commands/users.js +9 -35
  39. package/src/commands/version.js +3 -0
  40. package/src/errors.js +92 -1
  41. package/src/help.js +1 -0
  42. package/src/helpers.js +22 -11
  43. package/src/mutations.js +36 -91
  44. package/src/output.js +187 -61
  45. package/src/resolve-record.js +71 -0
  46. package/src/sdk.js +108 -8
  47. package/src/session.js +214 -0
@@ -1,7 +1,66 @@
1
1
  import { getEffectiveInstance, normalizeInstanceURL, saveConfig, setActiveProfile, setProfile } from '../config.js';
2
2
  import { errUsage } from '../errors.js';
3
+ import { declareCapabilities } from '../capabilities.js';
3
4
  import process from 'node:process';
4
5
 
6
+ // Auth manages local credentials/profiles — runs without a configured
7
+ // instance. (Not in the daily-check skip-list: legacy behavior checks for
8
+ // updates here.)
9
+ declareCapabilities('auth', { noInstance: true });
10
+
11
+ /**
12
+ * Render the `jsn auth status` styled (TTY) view: one line per profile with
13
+ * default marker, auth ✓/✗, 🔒 read_only, ⚡ skip_confirmations, ✅/⚠️
14
+ * live-verification, and stale-last-seen hints.
15
+ *
16
+ * This view logic lives in the auth command — NOT in src/output.js — and is
17
+ * shipped to the OutputWriter as `data._formatted` (the documented escape
18
+ * hatch for command-curated visuals; see the interface comment in
19
+ * src/output.js). The `summary` line is folded into the formatted string
20
+ * because the writer suppresses opts.summary when _formatted is present.
21
+ * Golden-pinned by test/auth.test.js ("auth status styled output").
22
+ */
23
+ export function renderAuthStatus(result, summary) {
24
+ let out = summary ? `${summary}\n\n` : '';
25
+ const profiles = Array.isArray(result.profiles) ? result.profiles : [];
26
+ if (profiles.length > 0) {
27
+ out += '\n';
28
+ for (const p of profiles) {
29
+ const prefix = p.default ? '* ' : ' ';
30
+ const authIcon = p.authenticated ? '✓' : '✗';
31
+
32
+ // Show verified status if we got one
33
+ let verifiedStr = '';
34
+ if (p.verified === true) {
35
+ verifiedStr = ' ✅';
36
+ } else if (p.verified === false) {
37
+ verifiedStr = ' ⚠️';
38
+ }
39
+
40
+ // Show stale hint if >7 days since last seen
41
+ let staleStr = '';
42
+ if (p.stale && p.days_since_last_seen) {
43
+ staleStr = ` (${p.days_since_last_seen}d ago — may have been released)`;
44
+ }
45
+
46
+ // Show lock icon for read-only profiles
47
+ const lockIcon = p.read_only ? ' 🔒' : '';
48
+
49
+ // Show confirmations-off badge for skip_confirmations profiles
50
+ const confirmBadge = p.skip_confirmations ? ' ⚡' : '';
51
+
52
+ if (p.name) {
53
+ out += `${prefix}${authIcon} ${p.name} — ${p.instance}${lockIcon}${confirmBadge}${verifiedStr}${staleStr}\n`;
54
+ } else if (p.username) {
55
+ out += `${prefix}${authIcon} ${p.instance} (as ${p.username})${lockIcon}${confirmBadge}${verifiedStr}${staleStr}\n`;
56
+ } else {
57
+ out += `${prefix}${authIcon} ${p.instance}${lockIcon}${confirmBadge}${verifiedStr}${staleStr}\n`;
58
+ }
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
5
64
  /**
6
65
  * Resolve a login/logout/refresh argument to a concrete instance URL.
7
66
  *
@@ -46,12 +105,15 @@ export async function pickProfile(app, message) {
46
105
  }
47
106
 
48
107
  /**
49
- * Resolve the instance for the wizard: only an EXPLICIT source (env var,
50
- * --instance flag/override) pre-fills. The config's default profile must
51
- * NOT — the wizard adds an instance, so it always asks otherwise.
108
+ * Resolve the instance for the wizard: only an EXPLICIT source (env var or
109
+ * a flag-provided override visible on the app session) pre-fills. The
110
+ * config's default profile must NOT — the wizard adds an instance, so it
111
+ * always asks otherwise.
52
112
  */
53
113
  export function resolveWizardInstance(app, argv = {}) {
54
- return process.env.SERVICENOW_INSTANCE_URL || app._overrideInstance || argv.instance || '';
114
+ // app.getEffectiveInstance() is the session-backed read (production App);
115
+ // the _overrideInstance fallback keeps hand-rolled test fakes honest.
116
+ return process.env.SERVICENOW_INSTANCE_URL || app.getEffectiveInstance?.() || app._overrideInstance || argv.instance || '';
55
117
  }
56
118
 
57
119
  /**
@@ -125,6 +187,18 @@ export async function loginWizard(app, argv = {}) {
125
187
  // Set as active profile so subsequent commands know which instance to use
126
188
  app.config.activeProfile = profileName;
127
189
  app.config.defaultProfile = profileName;
190
+
191
+ // Present all connection-level options during setup. Gated on interactivity
192
+ // so a piped/scripted call never blocks on prompts.
193
+ const { canPrompt } = await import('../helpers.js');
194
+ if (canPrompt()) {
195
+ const { confirm } = await import('@inquirer/prompts');
196
+ profile.read_only = await confirm({ message: 'Read-only profile (blocks mutation commands)?', default: profile.read_only || false });
197
+ profile.skip_confirmations = await confirm({ message: 'Skip confirmations (deletes run without prompting)?', default: profile.skip_confirmations || false });
198
+ profile.include_counts = await confirm({ message: 'Include result totals on list commands?', default: profile.include_counts !== false });
199
+ console.log('');
200
+ }
201
+
128
202
  await saveConfig(app.config);
129
203
 
130
204
  if (!useBasic) {
@@ -237,6 +311,7 @@ async function pickLoginTarget(app) {
237
311
  const PROFILE_FLAGS = [
238
312
  { name: 'Read-only (blocks mutation commands)', value: 'read_only', icon: '🔒' },
239
313
  { name: 'Skip confirmations (deletes run without prompting)', value: 'skip_confirmations', icon: '⚡' },
314
+ { name: 'Include result totals on list commands', value: 'include_counts', icon: '🔢' },
240
315
  ];
241
316
 
242
317
  /**
@@ -383,6 +458,10 @@ export function authCmd(wrap) {
383
458
  describe: 'Skip delete confirmations on this profile (deletes run without prompting)',
384
459
  type: 'boolean',
385
460
  default: false,
461
+ })
462
+ .option('include-counts', {
463
+ describe: 'Include result totals on list commands (default on; pass --no-include-counts to opt out)',
464
+ type: 'boolean',
386
465
  }),
387
466
  handler: wrap(async (argv, app) => {
388
467
  let instanceURL;
@@ -485,19 +564,15 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
485
564
  username: username || undefined,
486
565
  read_only: argv['read-only'] || undefined,
487
566
  skip_confirmations: argv['skip-confirmations'] || undefined,
567
+ include_counts: argv['include-counts'] === true ? true : argv['include-counts'] === false ? false : undefined,
488
568
  };
489
569
 
490
570
  // Re-save OAuth credentials with username now that we have it,
491
571
  // so they're keyed by <user>@<instance> for per-user isolation.
492
- // Also clean up the old bare-instance key so migration is clean.
572
+ // The legacy bare-instance key is cleaned up behind the
573
+ // AuthManager seam (credential-store internals don't leak here).
493
574
  if (!argv.password && username) {
494
- const { saveCredentials, loadCredentials, deleteCredentials } = await import('../auth.js');
495
- const stored = loadCredentials(instanceURL);
496
- if (stored && stored.access_token) {
497
- stored.username = username;
498
- saveCredentials(instanceURL, stored, username);
499
- deleteCredentials(instanceURL); // remove bare-instance legacy key
500
- }
575
+ app.auth.migrateLegacyCredential(instanceURL, username);
501
576
  }
502
577
 
503
578
  // Set as default if this is the first one
@@ -598,6 +673,7 @@ Examples:
598
673
  default: instance === defaultInstance,
599
674
  read_only: profile.read_only || false,
600
675
  skip_confirmations: profile.skip_confirmations || false,
676
+ include_counts: profile.include_counts !== false,
601
677
  });
602
678
  }
603
679
 
@@ -608,7 +684,11 @@ Examples:
608
684
  profiles,
609
685
  };
610
686
 
611
- app.ok(result, { summary: `${profiles.length} profile(s)` });
687
+ const summary = `${profiles.length} profile(s)`;
688
+ // Ship the styled view as _formatted (see renderAuthStatus); the
689
+ // OutputWriter prints it verbatim and suppresses the summary.
690
+ result._formatted = renderAuthStatus(result, summary);
691
+ app.ok(result, { summary });
612
692
 
613
693
  // Warn about legacy credentials that need re-authentication
614
694
  const legacyProfiles = profiles.filter(p => p.legacy);
@@ -0,0 +1,139 @@
1
+ // Catalog item enrichment — pure async data-shaping for `catalogitems show`.
2
+ //
3
+ // Given an sdk handle, an instance URL and an item sys_id, return the
4
+ // enriched item: core fields resolved to display values, flow name resolved
5
+ // through the sys_hub_flow fallback chain, workflow/delivery-plan fallback,
6
+ // and variables grouped by variable set. No yargs, no output formatting —
7
+ // callers own the envelope and rendering.
8
+
9
+ import { getStringField } from '../../helpers.js';
10
+
11
+ /**
12
+ * Fetch a catalog item and assemble the full show envelope.
13
+ *
14
+ * @param {object} deps
15
+ * @param {object} deps.sdk - SDK handle (list/get).
16
+ * @param {string} deps.instanceUrl - Effective instance URL (for the item link).
17
+ * @param {string} deps.sysID - sc_cat_item sys_id.
18
+ * @returns {Promise<{data: object, standalone: Array, setVars: Array, totalVars: number}>}
19
+ * data is the envelope payload (no _formatted — the caller attaches it);
20
+ * standalone/setVars/totalVars are the raw groupings needed by
21
+ * buildCatalogFormatted.
22
+ */
23
+ export async function enrichCatalogItem({ sdk, instanceUrl, sysID }) {
24
+ // Use list + display_value=all so reference fields (category, flow) resolve
25
+ // to readable display values instead of raw sys_ids.
26
+ const p = new URLSearchParams();
27
+ p.set('sysparm_query', `sys_id=${sysID}`);
28
+ p.set('sysparm_limit', '1');
29
+ p.set('sysparm_display_value', 'all');
30
+ const items = await sdk.list('sc_cat_item', p);
31
+ const item = items[0] || null;
32
+ if (!item) throw new Error(`Not found: ${sysID}`);
33
+
34
+ const vp = new URLSearchParams();
35
+ vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
36
+ vp.set('sysparm_limit', '100');
37
+ vp.set('sysparm_display_value', 'all');
38
+ vp.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
39
+ const variables = await sdk.list('item_option_new', vp);
40
+
41
+ const standalone = [];
42
+ const setVars = new Map();
43
+ for (const v of variables) {
44
+ const vs = getStringField(v, 'variable_set');
45
+ const entry = {
46
+ name: getStringField(v, 'name'),
47
+ question_text: getStringField(v, 'question_text'),
48
+ type: getStringField(v, 'type'),
49
+ mandatory: getStringField(v, 'mandatory'),
50
+ };
51
+ if (vs) {
52
+ if (!setVars.has(vs)) setVars.set(vs, { name: vs, variables: [] });
53
+ setVars.get(vs).variables.push(entry);
54
+ } else {
55
+ standalone.push(entry);
56
+ }
57
+ }
58
+ const totalVars = standalone.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0);
59
+
60
+ // Resolve flow name
61
+ let flowName = '', flowCmd = '';
62
+ const flowVal = item.flow_designer_flow;
63
+ if (flowVal?.value) {
64
+ const fn = getStringField(item, 'flow_designer_flow');
65
+ if (fn === flowVal.value) {
66
+ try { const fr = await sdk.get('sys_hub_flow', flowVal.value); flowName = getStringField(fr, 'name') || ''; } catch { flowName = fn; }
67
+ } else { flowName = fn; }
68
+ flowCmd = `jsn flows show ${flowVal.value}`;
69
+ }
70
+ const wfName = getStringField(item, 'workflow') || '';
71
+ const wfID = item.workflow?.value || '';
72
+ const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan') || '';
73
+
74
+ const data = {
75
+ name: getStringField(item, 'name'),
76
+ short_description: getStringField(item, 'short_description'),
77
+ category: getStringField(item, 'category'),
78
+ active: getStringField(item, 'active'),
79
+ url: `${instanceUrl}/sc_cat_item.do?sys_id=${sysID}`,
80
+ flow: flowName || undefined,
81
+ flow_cmd: flowCmd || undefined,
82
+ workflow: wfName || undefined,
83
+ workflow_cmd: (wfName && wfID) ? `jsn workflows show ${wfID}` : undefined,
84
+ execution_plan: planName || undefined,
85
+ variables: totalVars > 0 ? {
86
+ count: totalVars,
87
+ standalone: standalone.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })),
88
+ 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' })) })),
89
+ } : undefined,
90
+ };
91
+
92
+ return { data, standalone, setVars: Array.from(setVars.values()), totalVars };
93
+ }
94
+
95
+ /**
96
+ * Styled-mode rendering of an enriched catalog item. Kept verbatim from the
97
+ * original catalog.js — the output layer is being reworked elsewhere, so this
98
+ * stays byte-identical for now.
99
+ */
100
+ export function buildCatalogFormatted(data, standalone, setVars, totalVars) {
101
+ const lines = [];
102
+ lines.push(`${data.name} (sc_cat_item)`);
103
+ lines.push('');
104
+ lines.push('─ Details ─');
105
+ if (data.short_description) lines.push(` short_description: ${data.short_description}`);
106
+ if (data.category) lines.push(` category: ${data.category}`);
107
+ lines.push(` active: ${data.active}`);
108
+ if (data.url) lines.push(` url: ${data.url}`);
109
+ if (data.execution_plan) lines.push(` execution_plan: ${data.execution_plan}`);
110
+ lines.push('');
111
+ if (data.flow) {
112
+ lines.push('─ Flow ─');
113
+ lines.push(` ${data.flow}`);
114
+ if (data.flow_cmd) lines.push(` → ${data.flow_cmd}`);
115
+ lines.push('');
116
+ }
117
+ if (data.workflow) {
118
+ lines.push('─ Workflow ─');
119
+ lines.push(` ${data.workflow}`);
120
+ if (data.workflow_cmd) lines.push(` → ${data.workflow_cmd}`);
121
+ lines.push('');
122
+ }
123
+ if (totalVars > 0) {
124
+ lines.push('─ Catalog Variables ─');
125
+ for (const v of standalone) {
126
+ const label = v.question_text || v.name;
127
+ lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
128
+ }
129
+ for (const s of setVars) {
130
+ lines.push(` [${s.name}]`);
131
+ for (const v of s.variables) {
132
+ const label = v.question_text || v.name;
133
+ lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
134
+ }
135
+ }
136
+ lines.push('');
137
+ }
138
+ return lines.join('\n');
139
+ }
@@ -1,4 +1,9 @@
1
- import { getStringField, interactiveList, assertSafeExactMatch } from '../helpers.js';
1
+ import { getStringField, interactiveList, resolveItemOptionType } from '../helpers.js';
2
+ import { resolveSysId } from '../resolve-record.js';
3
+ import { declareCapabilities } from '../capabilities.js';
4
+ import { enrichCatalogItem, buildCatalogFormatted } from './catalog/enrich.js';
5
+
6
+ declareCapabilities('catalogitems', { mutationSubcommands: ['create'] });
2
7
 
3
8
  export function catalogCmd(wrap) {
4
9
  return {
@@ -24,16 +29,7 @@ export function catalogCmd(wrap) {
24
29
  if (argv.variables) {
25
30
  let varsObj;
26
31
  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
- }
32
+ const itemSysID = await resolveSysId(app.sdk, { table: 'sc_cat_item', identifier: argv.name, matchField: 'name', resource: 'Catalog item' });
37
33
  const endpoint = `${app.sdk.baseURL}/api/sn_sc/servicecatalog/items/${itemSysID}/submit_produce`;
38
34
  const result = await app.sdk.request(endpoint, { method: 'POST', body: JSON.stringify({ variables: varsObj }) });
39
35
  const reqID = result?.result?.sys_id || result?.result?.number || '';
@@ -71,9 +67,8 @@ export function catalogCmd(wrap) {
71
67
  const label = parts.length >= 3 ? parts[parts.length - 1] : parts[0];
72
68
  const sysName = parts[0].replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
73
69
  try {
74
- const typeMap = { string: '6', multilinetext: '2', select: '3', date: '4', datetime: '5', reference: '8', checkbox: '12', email: '11' };
75
70
  await app.sdk.create('item_option_new', {
76
- name: sysName, question_text: label, type: typeMap[typeName] || '6',
71
+ name: sysName, question_text: label, type: resolveItemOptionType(typeName),
77
72
  order: (varCount + 1) * 100, cat_item: itemID, mandatory: true, active: true,
78
73
  });
79
74
  varCount++;
@@ -148,17 +143,7 @@ export function catalogCmd(wrap) {
148
143
  describe: 'Show a catalog item',
149
144
  handler: wrap(async (argv, app) => {
150
145
  app.requireInstance();
151
- let id = argv.id;
152
- if (!/^[a-f0-9]{32}$/i.test(id)) {
153
- assertSafeExactMatch(id);
154
- const p = new URLSearchParams();
155
- p.set('sysparm_query', `name=${id}`);
156
- p.set('sysparm_limit', '1');
157
- p.set('sysparm_fields', 'sys_id');
158
- const results = await app.sdk.list('sc_cat_item', p);
159
- if (results.length === 0) throw new Error(`Not found: ${id}`);
160
- id = results[0].sys_id?.value || results[0].sys_id;
161
- }
146
+ const id = await resolveSysId(app.sdk, { table: 'sc_cat_item', identifier: argv.id, matchField: 'name', resource: 'Catalog item' });
162
147
  await showItem(app, id);
163
148
  }),
164
149
  });
@@ -175,77 +160,15 @@ export function catalogCmd(wrap) {
175
160
  }
176
161
 
177
162
  async function showItem(app, sysID) {
178
- // Use list + display_value=all so reference fields (category, flow) resolve
179
- // to readable display values instead of raw sys_ids.
180
- const p = new URLSearchParams();
181
- p.set('sysparm_query', `sys_id=${sysID}`);
182
- p.set('sysparm_limit', '1');
183
- p.set('sysparm_display_value', 'all');
184
- const items = await app.sdk.list('sc_cat_item', p);
185
- const item = items[0] || null;
186
- if (!item) throw new Error(`Not found: ${sysID}`);
187
-
188
- const vp = new URLSearchParams();
189
- vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
190
- vp.set('sysparm_limit', '100');
191
- vp.set('sysparm_display_value', 'all');
192
- vp.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
193
- const variables = await app.sdk.list('item_option_new', vp);
194
-
195
- const standalone = [];
196
- const setVars = new Map();
197
- for (const v of variables) {
198
- const vs = getStringField(v, 'variable_set');
199
- const entry = {
200
- name: getStringField(v, 'name'),
201
- question_text: getStringField(v, 'question_text'),
202
- type: getStringField(v, 'type'),
203
- mandatory: getStringField(v, 'mandatory'),
204
- };
205
- if (vs) {
206
- if (!setVars.has(vs)) setVars.set(vs, { name: vs, variables: [] });
207
- setVars.get(vs).variables.push(entry);
208
- } else {
209
- standalone.push(entry);
210
- }
211
- }
212
- const totalVars = standalone.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0);
213
-
214
- // Resolve flow name
215
- let flowName = '', flowCmd = '';
216
- const flowVal = item.flow_designer_flow;
217
- if (flowVal?.value) {
218
- const fn = getStringField(item, 'flow_designer_flow');
219
- if (fn === flowVal.value) {
220
- try { const fr = await app.sdk.get('sys_hub_flow', flowVal.value); flowName = getStringField(fr, 'name') || ''; } catch { flowName = fn; }
221
- } else { flowName = fn; }
222
- flowCmd = `jsn flows show ${flowVal.value}`;
223
- }
224
- const wfName = getStringField(item, 'workflow') || '';
225
- const wfID = item.workflow?.value || '';
226
- const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan') || '';
227
-
228
- const data = {
229
- name: getStringField(item, 'name'),
230
- short_description: getStringField(item, 'short_description'),
231
- category: getStringField(item, 'category'),
232
- active: getStringField(item, 'active'),
233
- url: `${app.getEffectiveInstance()}/sc_cat_item.do?sys_id=${sysID}`,
234
- flow: flowName || undefined,
235
- flow_cmd: flowCmd || undefined,
236
- workflow: wfName || undefined,
237
- workflow_cmd: (wfName && wfID) ? `jsn workflows show ${wfID}` : undefined,
238
- execution_plan: planName || undefined,
239
- variables: totalVars > 0 ? {
240
- count: totalVars,
241
- standalone: standalone.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })),
242
- 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' })) })),
243
- } : undefined,
244
- };
163
+ const { data, standalone, setVars, totalVars } = await enrichCatalogItem({
164
+ sdk: app.sdk,
165
+ instanceUrl: app.getEffectiveInstance(),
166
+ sysID,
167
+ });
245
168
 
246
169
  // Styled-mode rendering (non-enumerable so JSON output stays clean)
247
170
  Object.defineProperty(data, '_formatted', {
248
- value: buildCatalogFormatted(data, standalone, Array.from(setVars.values()), totalVars),
171
+ value: buildCatalogFormatted(data, standalone, setVars, totalVars),
249
172
  enumerable: false,
250
173
  configurable: true,
251
174
  });
@@ -257,44 +180,3 @@ async function showItem(app, sysID) {
257
180
  ],
258
181
  });
259
182
  }
260
-
261
- function buildCatalogFormatted(data, standalone, setVars, totalVars) {
262
- const lines = [];
263
- lines.push(`${data.name} (sc_cat_item)`);
264
- lines.push('');
265
- lines.push('─ Details ─');
266
- if (data.short_description) lines.push(` short_description: ${data.short_description}`);
267
- if (data.category) lines.push(` category: ${data.category}`);
268
- lines.push(` active: ${data.active}`);
269
- if (data.url) lines.push(` url: ${data.url}`);
270
- if (data.execution_plan) lines.push(` execution_plan: ${data.execution_plan}`);
271
- lines.push('');
272
- if (data.flow) {
273
- lines.push('─ Flow ─');
274
- lines.push(` ${data.flow}`);
275
- if (data.flow_cmd) lines.push(` → ${data.flow_cmd}`);
276
- lines.push('');
277
- }
278
- if (data.workflow) {
279
- lines.push('─ Workflow ─');
280
- lines.push(` ${data.workflow}`);
281
- if (data.workflow_cmd) lines.push(` → ${data.workflow_cmd}`);
282
- lines.push('');
283
- }
284
- if (totalVars > 0) {
285
- lines.push('─ Catalog Variables ─');
286
- for (const v of standalone) {
287
- const label = v.question_text || v.name;
288
- lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
289
- }
290
- for (const s of setVars) {
291
- lines.push(` [${s.name}]`);
292
- for (const v of s.variables) {
293
- const label = v.question_text || v.name;
294
- lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
295
- }
296
- }
297
- lines.push('');
298
- }
299
- return lines.join('\n');
300
- }