@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.
- package/README.md +18 -2
- package/package.json +4 -1
- package/src/app.js +49 -65
- package/src/auth.js +46 -11
- package/src/capabilities.js +128 -0
- package/src/cli.js +129 -79
- package/src/commands/_ticket.js +36 -92
- package/src/commands/approvals.js +350 -0
- package/src/commands/atf.js +635 -0
- package/src/commands/auth.js +93 -13
- package/src/commands/catalog/enrich.js +139 -0
- package/src/commands/catalog.js +15 -133
- package/src/commands/cmdb.js +613 -0
- package/src/commands/codesearch.js +103 -0
- package/src/commands/dev/_generic.js +62 -110
- package/src/commands/dev/_simple.js +16 -12
- package/src/commands/dev/b4rules.js +1 -1
- package/src/commands/dev/domains.js +4 -0
- package/src/commands/dev/eval.js +40 -4
- package/src/commands/dev/flows.js +3 -0
- package/src/commands/dev/logs.js +97 -0
- package/src/commands/dev/rest.js +6 -0
- package/src/commands/dev/restmessage.js +1 -0
- package/src/commands/dev/scopes.js +49 -25
- package/src/commands/dev/scrapi.js +5 -0
- package/src/commands/dev/soapmessages.js +1 -0
- package/src/commands/dev/updatesets.js +145 -15
- package/src/commands/dev.js +3 -2
- package/src/commands/docs/db.js +14 -0
- package/src/commands/docs/docs.js +9 -21
- package/src/commands/docs/search.js +33 -10
- package/src/commands/groups.js +3 -0
- package/src/commands/records.js +254 -9
- package/src/commands/setup.js +5 -0
- package/src/commands/skill.js +3 -0
- package/src/commands/snippets.js +155 -0
- package/src/commands/tickets.js +9 -29
- package/src/commands/users.js +9 -35
- package/src/commands/version.js +3 -0
- package/src/errors.js +92 -1
- package/src/help.js +1 -0
- package/src/helpers.js +22 -11
- package/src/mutations.js +36 -91
- package/src/output.js +187 -61
- package/src/resolve-record.js +71 -0
- package/src/sdk.js +108 -8
- package/src/session.js +214 -0
package/src/commands/auth.js
CHANGED
|
@@ -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
|
-
*
|
|
51
|
-
* NOT — the wizard adds an instance, so it
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/catalog.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { getStringField, interactiveList,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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,
|
|
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
|
-
}
|