@jacebenson/jsn 4.3.7 → 4.3.8
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 +246 -1
- package/package.json +1 -1
- package/src/app.js +8 -1
- package/src/auth.js +14 -12
- package/src/cli-adapter.js +162 -11
- package/src/cli.js +121 -13
- package/src/commands/_generic.js +3 -2
- package/src/commands/_simple.js +1 -1
- package/src/commands/actions.js +156 -0
- package/src/commands/auth.js +21 -11
- package/src/commands/cmdb.js +18 -0
- package/src/commands/diff.js +205 -0
- package/src/commands/docs/docs.js +18 -16
- package/src/commands/flows.js +149 -47
- package/src/commands/graphql.js +95 -0
- package/src/commands/groupmembers.js +19 -4
- package/src/commands/grouproles.js +15 -4
- package/src/commands/midservers.js +708 -0
- package/src/commands/perf.js +36 -9
- package/src/commands/platform.js +202 -0
- package/src/commands/records.js +19 -15
- package/src/commands/skill.js +5 -2
- package/src/commands/transactions.js +78 -0
- package/src/errors.js +71 -6
- package/src/flow-create.js +884 -0
- package/src/flow-designer-interactive.js +417 -0
- package/src/flow-inspection.js +15 -5
- package/src/flow-publish.js +32 -0
- package/src/flow-test.js +122 -0
- package/src/flow-triggers.js +448 -0
- package/src/flow-versioning.js +53 -0
- package/src/help.js +93 -28
- package/src/helpers.js +124 -9
- package/src/midserver-direct.js +312 -0
- package/src/midserver-install.js +170 -0
- package/src/midserver-zip.js +107 -0
- package/src/output.js +7 -0
- package/src/processflow-actions.js +366 -0
- package/src/sdk.js +8 -0
package/src/cli.js
CHANGED
|
@@ -10,7 +10,8 @@ import { resolveSession, applySession } from './session.js';
|
|
|
10
10
|
import { renderHelp } from './help.js';
|
|
11
11
|
import { isMutationCommand, refreshMutationCommands } from './mutations.js';
|
|
12
12
|
import { noInstanceCommands, dailyCheckSkipCommands } from './capabilities.js';
|
|
13
|
-
import { guardExit, exitWithError } from './errors.js';
|
|
13
|
+
import { guardExit, exitWithError, AppError, CodeUsage } from './errors.js';
|
|
14
|
+
import { formatCommandList } from './helpers.js';
|
|
14
15
|
|
|
15
16
|
// Command modules
|
|
16
17
|
import { setupCmd } from './commands/setup.js';
|
|
@@ -69,6 +70,41 @@ import { b4rulesCmd } from './commands/b4rules.js';
|
|
|
69
70
|
import { atfCmd } from './commands/atf.js';
|
|
70
71
|
import { approvalsCmd } from './commands/approvals.js';
|
|
71
72
|
import { perfCmd } from './commands/perf.js';
|
|
73
|
+
import { midserversCmd } from './commands/midservers.js';
|
|
74
|
+
import { diffCmd } from './commands/diff.js';
|
|
75
|
+
import { graphqlCmd } from './commands/graphql.js';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The output format the caller asked for, resolved from PARSED argv.
|
|
79
|
+
*
|
|
80
|
+
* One policy, two callers: the format middleware applies it once a command
|
|
81
|
+
* parses, and the parse-time failure handler applies it when parsing fails.
|
|
82
|
+
* They used to disagree — the failure path re-scanned the raw tokens (any
|
|
83
|
+
* `--json` anywhere, first `--format` match, no option termination, no values
|
|
84
|
+
* consumed by string options, no `--no-` negation), so the stream and the shape
|
|
85
|
+
* of a failure depended on which layer rejected the command instead of on the
|
|
86
|
+
* command line the caller typed.
|
|
87
|
+
*
|
|
88
|
+
* Precedence is the middleware's, unchanged: the shortcut flags beat `--format`,
|
|
89
|
+
* and among the shortcuts the one applied last wins
|
|
90
|
+
* (csv > markdown > styled > quiet > json).
|
|
91
|
+
*/
|
|
92
|
+
export function resolveRequestedFormat(argv) {
|
|
93
|
+
if (!argv) return '';
|
|
94
|
+
if (argv.csv) return 'csv';
|
|
95
|
+
if (argv.markdown) return 'markdown';
|
|
96
|
+
if (argv.styled) return 'styled';
|
|
97
|
+
if (argv.quiet) return 'quiet';
|
|
98
|
+
if (argv.json) return 'json';
|
|
99
|
+
return typeof argv.format === 'string' ? argv.format : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parent commands owned by another change. The adapter's discovery fallback
|
|
104
|
+
* (which gives a handler-less parent its child list instead of a silent exit 0)
|
|
105
|
+
* is skipped for these families so their own fix owns the behavior.
|
|
106
|
+
*/
|
|
107
|
+
const DISCOVERY_OWNED_ELSEWHERE = new Set(['auth', 'perf', 'skill', 'groupmembers', 'grouproles']);
|
|
72
108
|
|
|
73
109
|
function wrap(handler) {
|
|
74
110
|
return async (argv) => {
|
|
@@ -148,15 +184,14 @@ export function buildCLI() {
|
|
|
148
184
|
},
|
|
149
185
|
})
|
|
150
186
|
.middleware([
|
|
151
|
-
// Apply format option overrides
|
|
187
|
+
// Apply format option overrides. The resolution lives in
|
|
188
|
+
// resolveRequestedFormat so the parse-time failure path applies the
|
|
189
|
+
// identical policy instead of approximating it from raw tokens.
|
|
152
190
|
(argv) => {
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (argv.csv) argv.format = 'csv';
|
|
158
|
-
if (argv.format) {
|
|
159
|
-
app.output.setFormat(argv.format);
|
|
191
|
+
const requested = resolveRequestedFormat(argv);
|
|
192
|
+
if (requested) {
|
|
193
|
+
argv.format = requested;
|
|
194
|
+
app.output.setFormat(requested);
|
|
160
195
|
}
|
|
161
196
|
if (argv.get) {
|
|
162
197
|
app.output.setJqFilter(argv.get);
|
|
@@ -371,6 +406,9 @@ export function buildCLI() {
|
|
|
371
406
|
.command(atfCmd(wrap))
|
|
372
407
|
.command(approvalsCmd(wrap))
|
|
373
408
|
.command(perfCmd(wrap))
|
|
409
|
+
.command(midserversCmd(wrap))
|
|
410
|
+
.command(diffCmd(wrap))
|
|
411
|
+
.command(graphqlCmd(wrap))
|
|
374
412
|
.command(evalCmd(wrap))
|
|
375
413
|
.command(restCmd(wrap))
|
|
376
414
|
.command(skillCmd(wrap))
|
|
@@ -429,14 +467,48 @@ export function buildCLI() {
|
|
|
429
467
|
cliInstance.showCompletionScript();
|
|
430
468
|
},
|
|
431
469
|
})
|
|
470
|
+
.onParentDiscovery((argv, { path, subcommands }) => {
|
|
471
|
+
// Reached only for a parent command that declares no default handler of
|
|
472
|
+
// its own (records attachments, docs community, and any future parent) —
|
|
473
|
+
// the adapter used to fall through and exit 0 having printed nothing.
|
|
474
|
+
// Children come from the registry, description included, so this list
|
|
475
|
+
// cannot drift from the registrations the way a hand-written one did.
|
|
476
|
+
if (DISCOVERY_OWNED_ELSEWHERE.has(path[0])) return;
|
|
477
|
+
const canonical = path.join(' ');
|
|
478
|
+
const summary = `jsn ${canonical} — available subcommands.`;
|
|
479
|
+
// Same error boundary as every registered handler (AGENTS.md: handlers go
|
|
480
|
+
// through wrap()). Discovery runs outside any command handler, and the
|
|
481
|
+
// shared extractor throws on a `--get` path that does not resolve — an
|
|
482
|
+
// unwrapped callback rejected parse() into an entry point with no
|
|
483
|
+
// rejection handler, so a missing path printed a Node stack instead of the
|
|
484
|
+
// extraction_not_found envelope.
|
|
485
|
+
return wrap(async () => {
|
|
486
|
+
(argv.app || app).ok({
|
|
487
|
+
command: canonical,
|
|
488
|
+
subcommands,
|
|
489
|
+
_formatted: formatCommandList({
|
|
490
|
+
title: `jsn ${canonical}`,
|
|
491
|
+
commands: subcommands,
|
|
492
|
+
footer: `Run "jsn ${canonical} <command> --help" for details.`,
|
|
493
|
+
}),
|
|
494
|
+
}, { summary });
|
|
495
|
+
})(argv);
|
|
496
|
+
})
|
|
432
497
|
.help('help', 'Show help')
|
|
433
498
|
.version(false)
|
|
434
499
|
.strictCommands()
|
|
435
500
|
.strictOptions(false)
|
|
436
501
|
.fail((msg, err) => {
|
|
437
502
|
if (err) throw err;
|
|
503
|
+
const raw = process.argv.slice(2);
|
|
504
|
+
// A parse-time failure never reaches the format middleware, so resolve the
|
|
505
|
+
// requested format with the adapter's OWN tokenizer (same option specs,
|
|
506
|
+
// option termination, consumed values, repeated options, `--no-`
|
|
507
|
+
// negation) instead of re-scanning raw argv — one policy, one answer for
|
|
508
|
+
// this command line no matter which layer rejects it.
|
|
509
|
+
const formatArgv = cliInstance.parseGlobalOptions(raw);
|
|
510
|
+
const requestedFormat = resolveRequestedFormat(formatArgv);
|
|
438
511
|
if (msg === 'You must specify a command') {
|
|
439
|
-
const raw = process.argv.slice(2);
|
|
440
512
|
const hasLoneProfile = raw.some((a, i, arr) => {
|
|
441
513
|
if (a !== '--profile' && a !== '-p') return false;
|
|
442
514
|
const next = arr[i + 1];
|
|
@@ -452,11 +524,47 @@ export function buildCLI() {
|
|
|
452
524
|
}
|
|
453
525
|
process.exit(0);
|
|
454
526
|
}
|
|
455
|
-
|
|
527
|
+
// No command is a discovery request, not a failure: the human view
|
|
528
|
+
// stays exactly as it was (exit 0 + grouped help), and an explicit
|
|
529
|
+
// --json gets the same surface as machine-readable data instead of
|
|
530
|
+
// prose that a parser cannot read.
|
|
531
|
+
if (requestedFormat === 'json') {
|
|
532
|
+
app.output.setFormat('json');
|
|
533
|
+
// `--get` is jsn's jq substitute for every JSON shape (AGENTS.md), so
|
|
534
|
+
// the root discovery envelope honours it too: extract when the path
|
|
535
|
+
// resolves, and fail with the structured envelope when it does not,
|
|
536
|
+
// instead of always dumping the whole envelope.
|
|
537
|
+
if (typeof formatArgv?.get === 'string' && formatArgv.get) {
|
|
538
|
+
app.output.setJqFilter(formatArgv.get);
|
|
539
|
+
}
|
|
540
|
+
const { commands } = cliInstance.commandRegistry();
|
|
541
|
+
try {
|
|
542
|
+
app.ok({ commands }, {
|
|
543
|
+
summary: `${commands.length} registered commands — run "jsn <command> --json" for a command's subcommands.`,
|
|
544
|
+
});
|
|
545
|
+
} catch (discoveryError) {
|
|
546
|
+
exitWithError(discoveryError, { app });
|
|
547
|
+
}
|
|
548
|
+
process.exit(0);
|
|
549
|
+
}
|
|
550
|
+
process.stdout.write(renderHelp(loadConfig(), cliInstance.commandRegistry()) + '\n');
|
|
456
551
|
process.exit(0);
|
|
457
552
|
}
|
|
458
|
-
|
|
459
|
-
|
|
553
|
+
// Every other failure here (unknown command, missing required argument,
|
|
554
|
+
// invalid choice/number) is a usage failure. Route it through the same
|
|
555
|
+
// renderer as handler and guard failures: exit 2, and the documented
|
|
556
|
+
// error envelope on stdout when the caller is in JSON mode. Previously
|
|
557
|
+
// this wrote raw prose to stderr and exited 1, so the exit code and the
|
|
558
|
+
// output shape depended on which layer rejected the command.
|
|
559
|
+
if (requestedFormat) app.output.setFormat(requestedFormat);
|
|
560
|
+
let hint;
|
|
561
|
+
try {
|
|
562
|
+
const path = cliInstance._findPath(raw).path.map((n) => n.name).join(' ');
|
|
563
|
+
hint = `Run "jsn ${path ? `${path} ` : ''}--help" for usage.`;
|
|
564
|
+
} catch {
|
|
565
|
+
hint = 'Run "jsn --help" for the command list.';
|
|
566
|
+
}
|
|
567
|
+
exitWithError(new AppError(CodeUsage, msg, hint), { app });
|
|
460
568
|
});
|
|
461
569
|
|
|
462
570
|
// Snapshot the root command names (plus aliases) for the completion
|
package/src/commands/_generic.js
CHANGED
|
@@ -181,11 +181,12 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
181
181
|
}),
|
|
182
182
|
})
|
|
183
183
|
.command({
|
|
184
|
-
command: 'show <identifier>',
|
|
184
|
+
command: opts.resolveIdentifier ? 'show [identifier]' : 'show <identifier>',
|
|
185
185
|
aliases: ['get'],
|
|
186
186
|
describe: `Show ${vowelArticle(singular)} ${singular} by name or sys_id`,
|
|
187
187
|
handler: wrap(async (argv, app) => {
|
|
188
|
-
const id = argv.identifier;
|
|
188
|
+
const id = opts.resolveIdentifier ? await opts.resolveIdentifier(argv, app) : argv.identifier;
|
|
189
|
+
if (id === undefined) return;
|
|
189
190
|
// Only restrict sysparm_fields if showFields is explicitly set.
|
|
190
191
|
// Go version fetches all fields for show unless explicitly restricted.
|
|
191
192
|
const fields = (showFields && showFields.length > 0)
|
package/src/commands/_simple.js
CHANGED
|
@@ -45,7 +45,7 @@ async function fetchExtensionChain(sdk, record) {
|
|
|
45
45
|
return chain;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export
|
|
48
|
+
export { actionsCmd } from './actions.js';
|
|
49
49
|
|
|
50
50
|
export const includesCmd = (wrap) => buildDevCmd('includes', 'sys_script_include', ['include', 'si'], ['name', 'api_name', 'active', 'sys_scope'], wrap, { singular: 'script include', scopeValidation: true });
|
|
51
51
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { buildDevCmd } from './_generic.js';
|
|
2
|
+
import { formatCommandList, parseDataArg } from '../helpers.js';
|
|
3
|
+
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
4
|
+
import { declareCapabilities } from '../capabilities.js';
|
|
5
|
+
import { ACTION_TABLE, actionScope } from '../processflow-actions.js';
|
|
6
|
+
import { errUsage } from '../errors.js';
|
|
7
|
+
import { cancelled, pickDesignerRecord, designerScope, guidedActionCreate, guidedActionEdit, guidedActionTestInputs, interactiveDesigner, hasDefinitionData, confirmDesignerMutation } from '../flow-designer-interactive.js';
|
|
8
|
+
|
|
9
|
+
const ACTION_COMMANDS = [
|
|
10
|
+
{ name: 'list', description: 'List actions' },
|
|
11
|
+
{ name: 'show [identifier]', description: 'Show an action by name or sys_id' },
|
|
12
|
+
{ name: 'definition [identifier]', description: 'Read the full JSON definition including executable step_instances' },
|
|
13
|
+
{ name: 'step-types', description: 'Read step schema templates for a target scope' },
|
|
14
|
+
{ name: 'create', description: 'Create an action: guided in a terminal, full JSON definition in scripts' },
|
|
15
|
+
{ name: 'update [identifier]', description: 'Save a full definition through Process Flow (alias: edit)' },
|
|
16
|
+
{ name: 'test [identifier]', description: 'Execute an action and optionally wait for actual outputs' },
|
|
17
|
+
{ name: 'delete [identifier]', description: 'Delete an action parent explicitly' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const scopeOption = y => y.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id; inferred from existing action when omitted' });
|
|
21
|
+
const dataOptions = y => y
|
|
22
|
+
.option('data-file', { type: 'string', describe: 'Full action definition JSON file, not table fields' })
|
|
23
|
+
.option('data', { type: 'string', describe: 'Full action definition as JSON' });
|
|
24
|
+
async function actionID(argv, app) {
|
|
25
|
+
const identifier = await pickDesignerRecord(argv, app, ACTION_TABLE, 'action');
|
|
26
|
+
if (identifier === undefined) return undefined;
|
|
27
|
+
argv = { ...argv, identifier };
|
|
28
|
+
return unwrapSysId(await resolveRecord(app.sdk, { table: ACTION_TABLE, identifier: argv.identifier, matchField: 'name', resource: 'action' }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const actionsCmd = wrap => {
|
|
32
|
+
// Reuse the existing list/show picker and output contract only.
|
|
33
|
+
const base = buildDevCmd('actions', ACTION_TABLE, ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, {
|
|
34
|
+
singular: 'action', readOnly: true,
|
|
35
|
+
resolveIdentifier: (argv, app) => cancelled(() => pickDesignerRecord(argv, app, ACTION_TABLE, 'action')),
|
|
36
|
+
showBreadcrumbs: (_record, id) => [
|
|
37
|
+
{ action: 'definition', cmd: `jsn actions definition ${id}`, description: 'Read complete executable definition' },
|
|
38
|
+
{ action: 'list', cmd: 'jsn actions list', description: 'Back to actions' },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
declareCapabilities('actions', { mutationSubcommands: ['create', 'update', 'edit', 'delete', 'test'] });
|
|
42
|
+
// Retain the common wrap and output controller; domain failures get a
|
|
43
|
+
// structured error envelope and nonzero exit, never a success-shaped dispatch.
|
|
44
|
+
const run = handler => wrap(async (argv, app) => {
|
|
45
|
+
try { await cancelled(() => handler(argv, app)); }
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!error.code?.startsWith('action_')) throw error;
|
|
48
|
+
if (error.details) error.hint = JSON.stringify(error.details);
|
|
49
|
+
app.err(error);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
...base,
|
|
55
|
+
describe: 'Flow Designer actions: create, edit full definitions, and test with outputs',
|
|
56
|
+
builder: y => base.builder(y)
|
|
57
|
+
.command({
|
|
58
|
+
command: 'create', describe: 'Create parent and executable Process Flow action; verify persistence',
|
|
59
|
+
builder: yy => dataOptions(yy).option('scope', { type: 'string', describe: 'Exact target scope sys_id, or global' }),
|
|
60
|
+
handler: run(async (argv, app) => {
|
|
61
|
+
const guided = !hasDefinitionData(argv) ? await guidedActionCreate(argv, app) : undefined;
|
|
62
|
+
const definition = guided?.definition ?? parseDataArg(argv);
|
|
63
|
+
const scope = guided?.scope ?? await designerScope(argv, app);
|
|
64
|
+
const result = await app.sdk.createProcessFlowAction(scope, definition);
|
|
65
|
+
app.ok(result, { summary: `Created and verified action ${result.sys_id}` });
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
.command({
|
|
69
|
+
command: 'definition [identifier]', describe: 'Read full JSON definition including executable step_instances',
|
|
70
|
+
builder: scopeOption,
|
|
71
|
+
handler: run(async (argv, app) => {
|
|
72
|
+
const id = await actionID(argv, app);
|
|
73
|
+
if (id === undefined) return;
|
|
74
|
+
const scope = await actionScope(app.sdk, id, argv.scope);
|
|
75
|
+
app.ok(await app.sdk.getProcessFlowAction(id, scope), { summary: `Read action definition ${id}` });
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
.command({
|
|
79
|
+
command: 'update [identifier]', aliases: ['edit'], describe: 'Save a full definition through Process Flow PUT and verify readback',
|
|
80
|
+
builder: yy => dataOptions(scopeOption(yy)),
|
|
81
|
+
handler: run(async (argv, app) => {
|
|
82
|
+
if (!hasDefinitionData(argv) && !interactiveDesigner(argv, app)) throw errUsage('Provide --data-file or --data for the full action definition in noninteractive mode');
|
|
83
|
+
const id = await actionID(argv, app);
|
|
84
|
+
if (id === undefined) return;
|
|
85
|
+
const definition = hasDefinitionData(argv) ? parseDataArg(argv)
|
|
86
|
+
: await guidedActionEdit(argv, app, await app.sdk.getProcessFlowAction(id, await actionScope(app.sdk, id, argv.scope)));
|
|
87
|
+
const result = await app.sdk.updateProcessFlowAction(id, argv.scope, definition);
|
|
88
|
+
app.ok(result, { summary: `Saved and verified action ${id}` });
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
.command({
|
|
92
|
+
command: 'test [identifier]', describe: 'Execute saved or supplied definition; optionally wait for actual outputs',
|
|
93
|
+
builder: yy => dataOptions(scopeOption(yy))
|
|
94
|
+
.option('force', { type: 'boolean', default: false, describe: 'Confirm execution of action scripts, which may have side effects' })
|
|
95
|
+
.option('output-map', { type: 'string', describe: 'JSON action input names to values; sent as outputMap' })
|
|
96
|
+
.option('run-on-thread', { type: 'boolean', default: true, describe: 'Run test on a worker thread' })
|
|
97
|
+
.option('tracing-enabled', { type: 'boolean', default: false, describe: 'Enable Flow Designer tracing' })
|
|
98
|
+
.option('wait', { type: 'boolean', default: false, describe: 'Wait for COMPLETE context and actual runtime outputs' })
|
|
99
|
+
.option('timeout', { type: 'number', default: 60, describe: 'Maximum wait seconds after dispatch, greater than 0, at most 3600' }),
|
|
100
|
+
handler: run(async (argv, app) => {
|
|
101
|
+
let outputMap;
|
|
102
|
+
try { outputMap = JSON.parse(argv['output-map'] ?? '{}'); }
|
|
103
|
+
catch { throw errUsage('--output-map must be valid JSON'); }
|
|
104
|
+
let definition = hasDefinitionData(argv) ? parseDataArg(argv) : undefined;
|
|
105
|
+
const id = await actionID(argv, app);
|
|
106
|
+
if (id === undefined) return;
|
|
107
|
+
if (argv['output-map'] === undefined && interactiveDesigner(argv, app)) {
|
|
108
|
+
definition = await app.sdk.getProcessFlowAction(id, await actionScope(app.sdk, id, argv.scope));
|
|
109
|
+
outputMap = await guidedActionTestInputs(argv, app, definition);
|
|
110
|
+
}
|
|
111
|
+
await confirmDesignerMutation(argv, app, `Execute action '${id}' scripts, which may have side effects`);
|
|
112
|
+
const result = await app.sdk.testProcessFlowAction(id, argv.scope, definition, outputMap, {
|
|
113
|
+
wait: argv.wait, timeout: argv.timeout, runOnThread: argv['run-on-thread'], tracingEnabled: argv['tracing-enabled'],
|
|
114
|
+
});
|
|
115
|
+
app.ok(result, { summary: result.status === 'complete' ? `Test completed ${result.context}` : `Test dispatched ${result.context}; outputs not yet verified` });
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
.command({
|
|
119
|
+
command: 'step-types', describe: 'Read step schema templates for a target scope',
|
|
120
|
+
builder: yy => yy.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id, or global' }),
|
|
121
|
+
handler: run(async (argv, app) => {
|
|
122
|
+
const scope = await designerScope(argv, app);
|
|
123
|
+
app.ok(await app.sdk.getProcessFlowStepTypes(scope), { summary: 'Process Flow step schemas' });
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
.command({
|
|
127
|
+
command: 'delete [identifier]', describe: 'Delete action parent explicitly',
|
|
128
|
+
builder: yy => scopeOption(yy).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
129
|
+
handler: run(async (argv, app) => {
|
|
130
|
+
const id = await actionID(argv, app);
|
|
131
|
+
if (id === undefined) return;
|
|
132
|
+
await actionScope(app.sdk, id, argv.scope);
|
|
133
|
+
await confirmDesignerMutation(argv, app, `Delete action '${id}'`);
|
|
134
|
+
await app.sdk.delete(ACTION_TABLE, id);
|
|
135
|
+
// Verify the exact target is absent. Do not interpret a 403 as deletion.
|
|
136
|
+
const query = new URLSearchParams({ sysparm_query: `sys_id=${id}`, sysparm_limit: '1', sysparm_fields: 'sys_id', sysparm_display_value: 'false' });
|
|
137
|
+
const response = await app.sdk.request(`${app.sdk.baseURL}/api/now/table/${ACTION_TABLE}?${query}`, { method: 'GET' });
|
|
138
|
+
if (!response || response.error || !Array.isArray(response.result) || response.result.length) {
|
|
139
|
+
throw new Error(`Delete is unverified; action ${id} absence was not explicitly confirmed`);
|
|
140
|
+
}
|
|
141
|
+
app.ok({ name: argv.identifier, sys_id: id, deleted: true }, { summary: `Deleted action ${id}` });
|
|
142
|
+
}),
|
|
143
|
+
}),
|
|
144
|
+
handler: wrap(async (_argv, app) => {
|
|
145
|
+
const summary = 'Manage Flow Designer actions from the sys_hub_action_type_definition table. Create and update guide terminal users; automation passes full JSON definitions, not table fields.';
|
|
146
|
+
app.ok({
|
|
147
|
+
commands: ACTION_COMMANDS.map(command => command.name),
|
|
148
|
+
_formatted: formatCommandList({
|
|
149
|
+
title: summary,
|
|
150
|
+
commands: ACTION_COMMANDS,
|
|
151
|
+
footer: 'Run "jsn actions <command> --help" for details. In a terminal you can omit the target and pick one.',
|
|
152
|
+
}),
|
|
153
|
+
}, { summary });
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
};
|
package/src/commands/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getEffectiveInstance, normalizeInstanceURL, saveConfig, setActiveProfile, setProfile } from '../config.js';
|
|
2
|
-
import { errUsage } from '../errors.js';
|
|
2
|
+
import { errUsage, errUsageHint } from '../errors.js';
|
|
3
|
+
import { canUsePicker } from '../helpers.js';
|
|
3
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
4
5
|
import process from 'node:process';
|
|
5
6
|
|
|
@@ -128,14 +129,16 @@ export function validateLoginSelectors(argv = {}, targetProfile = null) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
|
-
* Pick a profile by name — interactive search picker when ambiguous
|
|
132
|
-
* auto-selects when there's exactly one, throws when
|
|
132
|
+
* Pick a profile by name — interactive search picker when ambiguous and a
|
|
133
|
+
* human is available, auto-selects when there's exactly one, throws when
|
|
134
|
+
* none configured. A non-interactive caller (agent, pipe, --json/--get)
|
|
135
|
+
* never opens a picker; it falls back to the single/default profile.
|
|
133
136
|
*/
|
|
134
137
|
export async function pickProfile(app, message) {
|
|
135
138
|
const { search } = await import('@inquirer/prompts');
|
|
136
139
|
const profileNames = Object.keys(app.config.profiles || {});
|
|
137
140
|
if (profileNames.length === 0) throw errUsage('No profiles configured. Run "jsn setup" first.');
|
|
138
|
-
if (profileNames.length === 1 || !app
|
|
141
|
+
if (profileNames.length === 1 || !canUsePicker(app, {})) return profileNames[0];
|
|
139
142
|
const choices = profileNames.map(name => ({
|
|
140
143
|
name: `${name} — ${app.config.profiles[name].instance_url}`,
|
|
141
144
|
value: name,
|
|
@@ -281,10 +284,9 @@ export async function loginWizard(app, argv = {}) {
|
|
|
281
284
|
app.config.activeProfile = profileName;
|
|
282
285
|
app.config.defaultProfile = profileName;
|
|
283
286
|
|
|
284
|
-
// Present all connection-level options during setup. Gated on
|
|
285
|
-
// so a piped/scripted call never blocks on prompts.
|
|
286
|
-
|
|
287
|
-
if (canPrompt()) {
|
|
287
|
+
// Present all connection-level options during setup. Gated on
|
|
288
|
+
// interactivity so a piped/scripted call never blocks on prompts.
|
|
289
|
+
if (canUsePicker(app, argv)) {
|
|
288
290
|
// @inquirer/prompts owns stdin while these confirmations run. Close the
|
|
289
291
|
// older readline interface before handing stdin to Inquirer. This matters
|
|
290
292
|
// on Windows Git Bash: leaving both readers attached can make the later
|
|
@@ -604,7 +606,7 @@ export function authCmd(wrap) {
|
|
|
604
606
|
instanceURL = normalizeInstanceURL(namedProfile.instance_url);
|
|
605
607
|
} else if (argv.instance) {
|
|
606
608
|
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
607
|
-
} else if (app
|
|
609
|
+
} else if (canUsePicker(app, argv)) {
|
|
608
610
|
// gh-faithful: interactive terminals always get a picker —
|
|
609
611
|
// existing profiles plus "+ Add a new instance". Zero profiles
|
|
610
612
|
// goes straight to the setup wizard.
|
|
@@ -987,7 +989,9 @@ Examples:
|
|
|
987
989
|
if (profileNames.length === 0) {
|
|
988
990
|
throw errUsage('No profiles configured. Run "jsn auth login" first.');
|
|
989
991
|
}
|
|
990
|
-
if (profileNames.length
|
|
992
|
+
if (profileNames.length === 1) {
|
|
993
|
+
name = profileNames[0];
|
|
994
|
+
} else if (canUsePicker(app, argv)) {
|
|
991
995
|
const { search } = await import('@inquirer/prompts');
|
|
992
996
|
const choices = profileNames.map(profileName => ({
|
|
993
997
|
name: `${profileName} — ${app.config.profiles[profileName].instance_url}`,
|
|
@@ -1002,7 +1006,13 @@ Examples:
|
|
|
1002
1006
|
});
|
|
1003
1007
|
name = selected;
|
|
1004
1008
|
} else {
|
|
1005
|
-
|
|
1009
|
+
// Never silently switch the active profile for a
|
|
1010
|
+
// non-interactive caller: picking one for them is a
|
|
1011
|
+
// config write they did not ask for.
|
|
1012
|
+
throw errUsageHint(
|
|
1013
|
+
`Which profile? ${profileNames.length} profiles are configured: ${profileNames.join(', ')}`,
|
|
1014
|
+
'Pass the profile name: jsn auth switch <name>'
|
|
1015
|
+
);
|
|
1006
1016
|
}
|
|
1007
1017
|
}
|
|
1008
1018
|
await setActiveProfile(app.config, name);
|
package/src/commands/cmdb.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
assertSafeExactMatch,
|
|
10
10
|
buildQuerySuffix,
|
|
11
|
+
formatCommandList,
|
|
11
12
|
formatRecordForDisplay,
|
|
12
13
|
getStringField,
|
|
13
14
|
interactiveList,
|
|
@@ -351,6 +352,23 @@ export function cmdbCmd(wrap) {
|
|
|
351
352
|
return {
|
|
352
353
|
command: 'cmdb [subcommand]',
|
|
353
354
|
describe: 'Query the Configuration Management Database (CMDB) — CIs and relationship traversal (read-only)',
|
|
355
|
+
// Parent discovery. Without a handler `jsn cmdb` exited 0 having printed
|
|
356
|
+
// nothing: a script reads that as success and a human as a hang. The child
|
|
357
|
+
// list comes from the registry (argv.subcommands), so it cannot drift from
|
|
358
|
+
// what is registered; the styled view is a side channel that JSON ignores.
|
|
359
|
+
handler: wrap(async (argv, app) => {
|
|
360
|
+
const subcommands = argv.subcommands || [];
|
|
361
|
+
const summary = 'Query the Configuration Management Database (CMDB): CI lookup and relationship traversal. Read-only.';
|
|
362
|
+
app.ok({
|
|
363
|
+
command: 'cmdb',
|
|
364
|
+
subcommands,
|
|
365
|
+
_formatted: formatCommandList({
|
|
366
|
+
title: summary,
|
|
367
|
+
commands: subcommands,
|
|
368
|
+
footer: 'Run "jsn cmdb <command> --help" for details.',
|
|
369
|
+
}),
|
|
370
|
+
}, { summary });
|
|
371
|
+
}),
|
|
354
372
|
builder: (yargs) => {
|
|
355
373
|
return yargs
|
|
356
374
|
.command({
|