@jacebenson/jsn 4.3.7-dev.40 → 4.3.7-dev.43
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 +1 -1
- package/src/app.js +8 -1
- package/src/cli-adapter.js +96 -4
- package/src/cli.js +101 -5
- package/src/commands/auth.js +21 -11
- package/src/commands/cmdb.js +18 -0
- package/src/commands/docs/docs.js +18 -16
- package/src/commands/groupmembers.js +19 -4
- package/src/commands/grouproles.js +15 -4
- package/src/commands/perf.js +30 -9
- package/src/commands/platform.js +17 -0
- package/src/commands/records.js +19 -15
- package/src/commands/skill.js +5 -2
- package/src/errors.js +69 -9
- package/src/flow-create.js +26 -4
- package/src/flow-inspection.js +15 -5
- package/src/flow-test.js +6 -6
- package/src/flow-triggers.js +53 -0
- package/src/help.js +93 -28
- package/src/helpers.js +109 -9
- package/src/output.js +7 -0
package/src/helpers.js
CHANGED
|
@@ -32,6 +32,29 @@ export function canPrompt() {
|
|
|
32
32
|
return isTTY(process.stdout) && isTTY(process.stdin);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* True when an interactive picker may be opened for THIS invocation.
|
|
37
|
+
*
|
|
38
|
+
* canPrompt() alone answers "is a human reachable" — it does not know that
|
|
39
|
+
* the caller asked for machine output (`--json`, `--quiet`, `--csv`, …) or
|
|
40
|
+
* a one-shot extraction (`--get`). Opening a picker in either case blocks a
|
|
41
|
+
* script and corrupts the requested output shape, so every picker gate
|
|
42
|
+
* routes through here.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} app — app instance (uses app.output when present)
|
|
45
|
+
* @param {object} [argv] — parsed args (checks argv.get)
|
|
46
|
+
* @returns {boolean} true when a picker may be shown
|
|
47
|
+
*/
|
|
48
|
+
export function canUsePicker(app, argv = {}) {
|
|
49
|
+
if (argv && argv.get) return false;
|
|
50
|
+
if (!canPrompt()) return false;
|
|
51
|
+
const writer = app?.output;
|
|
52
|
+
if (!writer) return true; // no output controller (unit-test app) — canPrompt decided
|
|
53
|
+
const format = typeof writer.getFormat === 'function' ? writer.getFormat() : undefined;
|
|
54
|
+
if (!format) return true;
|
|
55
|
+
return format === FormatAuto;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Render a parent command's subcommand list for its default handler.
|
|
37
60
|
*
|
|
@@ -47,6 +70,55 @@ export function formatCommandList({ title, commands, footer }) {
|
|
|
47
70
|
return `${lines.join('\n')}\n`;
|
|
48
71
|
}
|
|
49
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Quote one argv token so the emitted confirmation hint is copy-pasteable
|
|
75
|
+
* in a POSIX shell. Boring tokens stay bare; anything containing spaces,
|
|
76
|
+
* quotes, `$`, `;`, `&`, `*` … gets single-quoted with embedded quotes
|
|
77
|
+
* escaped. `^ = < > ~ ! ,` are ServiceNow query characters — common in
|
|
78
|
+
* encoded queries — and are shell-safe bare, so they stay bare.
|
|
79
|
+
*
|
|
80
|
+
* @param {*} value — token
|
|
81
|
+
* @returns {string} shell-safe token
|
|
82
|
+
*/
|
|
83
|
+
export function quoteShellArg(value) {
|
|
84
|
+
const s = value == null ? '' : String(value);
|
|
85
|
+
if (s === '') return "''";
|
|
86
|
+
if (/^[A-Za-z0-9_@%+=:,./^-]+$/.test(s)) return s;
|
|
87
|
+
return `'${s.split("'").join("'\\''")}'`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the exact, safely-quoted rerun command for a confirmation hint.
|
|
92
|
+
*
|
|
93
|
+
* `argv._` holds command PATH NAMES only — the local adapter stores
|
|
94
|
+
* positionals and flags in named fields — so a hint built from `argv._`
|
|
95
|
+
* alone loses the target (`jsn incidents delete` with no number). Callers
|
|
96
|
+
* therefore pass the complete target tokens.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} argv — parsed args (reads `_`, `profile`, `instance`)
|
|
99
|
+
* @param {object} [opts]
|
|
100
|
+
* @param {string|string[]} [opts.rerun] — complete rerun command: a token
|
|
101
|
+
* array (quoted here) or a full command string (used verbatim)
|
|
102
|
+
* @returns {string} command without `--force`, or '' when unknowable
|
|
103
|
+
*/
|
|
104
|
+
export function buildRerunCommand(argv = {}, { rerun = null } = {}) {
|
|
105
|
+
let parts;
|
|
106
|
+
if (Array.isArray(rerun) && rerun.length > 0) {
|
|
107
|
+
parts = ['jsn', ...rerun].map(quoteShellArg);
|
|
108
|
+
} else if (typeof rerun === 'string' && rerun.trim()) {
|
|
109
|
+
parts = [rerun.trim()];
|
|
110
|
+
} else {
|
|
111
|
+
parts = ['jsn', ...(argv?._ || [])].map(quoteShellArg);
|
|
112
|
+
}
|
|
113
|
+
if (parts.length === 1) return ''; // no path known — nothing worth repeating
|
|
114
|
+
const joined = parts.join(' ');
|
|
115
|
+
// Re-pin the profile/instance the caller was operating on, so following
|
|
116
|
+
// the hint cannot silently retarget a different instance or profile.
|
|
117
|
+
if (argv?.profile && !joined.includes('--profile')) parts.push('--profile', quoteShellArg(argv.profile));
|
|
118
|
+
if (argv?.instance && !joined.includes('--instance')) parts.push('--instance', quoteShellArg(argv.instance));
|
|
119
|
+
return parts.join(' ');
|
|
120
|
+
}
|
|
121
|
+
|
|
50
122
|
/**
|
|
51
123
|
* Confirm a destructive action before executing.
|
|
52
124
|
*
|
|
@@ -59,10 +131,12 @@ export function formatCommandList({ title, commands, footer }) {
|
|
|
59
131
|
* @param {object} app — app instance (reads active profile from app.config)
|
|
60
132
|
* @param {object} argv — parsed args (checks argv.force)
|
|
61
133
|
* @param {string} question — e.g. `Delete incident INC0010001?`
|
|
134
|
+
* @param {object} [options] — { rerun } complete rerun command or token
|
|
135
|
+
* array for the hint (see buildRerunCommand)
|
|
62
136
|
* @returns {Promise<boolean>} true when the action may proceed
|
|
63
137
|
* @throws when confirmation is required but denied/unavailable
|
|
64
138
|
*/
|
|
65
|
-
export async function confirmDelete(app, argv, question) {
|
|
139
|
+
export async function confirmDelete(app, argv, question, options = {}) {
|
|
66
140
|
const profile = getActiveProfile(app.config);
|
|
67
141
|
if (profile?.skip_confirmations === true) return true;
|
|
68
142
|
if (argv.force) return true;
|
|
@@ -80,8 +154,7 @@ export async function confirmDelete(app, argv, question) {
|
|
|
80
154
|
}
|
|
81
155
|
// No human available (pipes, scripts, AI agents). Fail fast with a
|
|
82
156
|
// structured error an agent can act on — never hang waiting for stdin.
|
|
83
|
-
|
|
84
|
-
throw errConfirmationRequired(question, cmd);
|
|
157
|
+
throw errConfirmationRequired(question, buildRerunCommand(argv, options));
|
|
85
158
|
}
|
|
86
159
|
|
|
87
160
|
|
|
@@ -298,14 +371,39 @@ export async function interactiveList({ app, table, singular, columns, limit = 5
|
|
|
298
371
|
const pickerFields = pickerColumns.join(',');
|
|
299
372
|
const baseQuery = query ? query + '^' : '';
|
|
300
373
|
|
|
301
|
-
// Get total count
|
|
302
|
-
|
|
374
|
+
// Get total count. A FAILED count is unknown — never zero. Mapping the
|
|
375
|
+
// failure to 0 used to make callers report an invented "empty table"
|
|
376
|
+
// success (or silently return) when the real cause was a 403/network
|
|
377
|
+
// error. Only a measured 0 means empty; otherwise confirm emptiness with
|
|
378
|
+
// one bounded list request before showing the picker.
|
|
379
|
+
let totalCount = null;
|
|
380
|
+
let countError = null;
|
|
303
381
|
try {
|
|
304
382
|
totalCount = await app.sdk.aggregateCount(table, query || '');
|
|
305
|
-
} catch {
|
|
306
|
-
|
|
383
|
+
} catch (err) {
|
|
384
|
+
countError = err;
|
|
385
|
+
}
|
|
386
|
+
if (totalCount === 0) return null; // measured empty
|
|
387
|
+
|
|
388
|
+
if (countError) {
|
|
389
|
+
try {
|
|
390
|
+
const probeParams = new URLSearchParams();
|
|
391
|
+
probeParams.set('sysparm_limit', '1');
|
|
392
|
+
probeParams.set('sysparm_display_value', 'all');
|
|
393
|
+
probeParams.set('sysparm_fields', 'sys_id');
|
|
394
|
+
if (query) probeParams.set('sysparm_query', query);
|
|
395
|
+
const probe = await app.sdk.list(table, probeParams);
|
|
396
|
+
if (!Array.isArray(probe)) throw new Error('list returned a non-array result');
|
|
397
|
+
if (probe.length === 0) return null; // the list itself confirms empty
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// Both the count and the list failed — surface the failure instead of
|
|
400
|
+
// reporting an empty result nobody measured.
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Could not list ${table}: ${err.message || err} (aggregate count also failed: ${countError.message || countError})`,
|
|
403
|
+
{ cause: err }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
307
406
|
}
|
|
308
|
-
if (totalCount === 0) return null;
|
|
309
407
|
|
|
310
408
|
// Paginated source adapter: (term, offset, { signal }) => choices[]
|
|
311
409
|
async function serverSource(term, offset, { signal: _signal } = {}) {
|
|
@@ -418,7 +516,9 @@ export function parseDataArg(argv) {
|
|
|
418
516
|
try {
|
|
419
517
|
return JSON.parse(raw);
|
|
420
518
|
} catch (e) {
|
|
421
|
-
|
|
519
|
+
// Never echo the raw payload: real --data bodies carry personal data and
|
|
520
|
+
// secrets, and the parse position from e.message is enough to debug.
|
|
521
|
+
throw new Error(`Invalid JSON: ${e.message}\n\nHint: Use --data-file for multiline payloads to avoid shell escaping issues.`, { cause: e });
|
|
422
522
|
}
|
|
423
523
|
}
|
|
424
524
|
|
package/src/output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Output formatting: JSON, Markdown, Styled, Quiet
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import { errExtractionNotFound } from './errors.js';
|
|
4
5
|
|
|
5
6
|
export const FormatAuto = 'auto';
|
|
6
7
|
export const FormatJSON = 'json';
|
|
@@ -125,9 +126,15 @@ export class OutputWriter {
|
|
|
125
126
|
// --get extractor: resolve against the JSON envelope and print just that
|
|
126
127
|
// value as JSON. Takes precedence over styling/table formats — a path
|
|
127
128
|
// extract is inherently scriptable, not presentational.
|
|
129
|
+
//
|
|
130
|
+
// A path that does not resolve is an error, not a value. Stringifying
|
|
131
|
+
// `undefined` printed the literal token `undefined` and exited 0, so a
|
|
132
|
+
// typo looked like a successful empty result until the next tool in the
|
|
133
|
+
// pipeline failed to parse it. Present null/false/0/"" still print as-is.
|
|
128
134
|
if (this.jqFilter) {
|
|
129
135
|
const envelope = { ok: true, data, summary: opts.summary || '', breadcrumbs: opts.breadcrumbs || [] };
|
|
130
136
|
const value = resolvePath(envelope, this.jqFilter);
|
|
137
|
+
if (value === undefined) throw errExtractionNotFound(this.jqFilter);
|
|
131
138
|
this.writer.write(JSON.stringify(value, null, 2) + '\n');
|
|
132
139
|
return;
|
|
133
140
|
}
|