@jacebenson/jsn 4.3.7 → 4.3.8-dev.47
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 +266 -1
- package/package.json +1 -1
- package/skills/servicenow/SKILL.md +16 -0
- 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/commands/updatesets.js +77 -11
- 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-adapter.js
CHANGED
|
@@ -10,7 +10,11 @@ class CommandNode {
|
|
|
10
10
|
constructor(definition) {
|
|
11
11
|
this.definition = typeof definition === 'string' ? { command: definition } : definition;
|
|
12
12
|
this.name = commandName(this.definition.command || '');
|
|
13
|
-
|
|
13
|
+
// A command that lists its canonical name as its own alias publishes one
|
|
14
|
+
// token twice: the registry then has two entries for it and dispatch can
|
|
15
|
+
// resolve it through the alias table instead of the command table. An alias
|
|
16
|
+
// is an ALTERNATE name, so a self-alias is dropped at the source.
|
|
17
|
+
this.aliases = names(this.definition.aliases || this.definition.alias).filter((alias) => alias !== this.name);
|
|
14
18
|
this.children = [];
|
|
15
19
|
this.options = new Map();
|
|
16
20
|
this.positionals = new Map();
|
|
@@ -24,11 +28,56 @@ class CommandNode {
|
|
|
24
28
|
if (typeof this.definition.builder === 'function') this.definition.builder(this.api());
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Register a child, replacing any existing registration that claims the same
|
|
33
|
+
* canonical name (or that the new child claims as an alias).
|
|
34
|
+
*
|
|
35
|
+
* yargs semantics are last-registration-wins: a command module re-registers a
|
|
36
|
+
* subcommand to layer a specialized handler over the base one (see
|
|
37
|
+
* `requests show`). Appending instead of replacing left the base registration
|
|
38
|
+
* first in the list and reachable by dispatch/help while the duplicate was
|
|
39
|
+
* invisible to the registry (child counts, inventory, completion).
|
|
40
|
+
*
|
|
41
|
+
* Registration order is preserved (the slot is replaced in place) and the
|
|
42
|
+
* replaced node's aliases are carried over so layered commands keep resolving
|
|
43
|
+
* through the alias that the base registration published.
|
|
44
|
+
*
|
|
45
|
+
* A token must resolve to exactly ONE command. Two registrations can also
|
|
46
|
+
* collide without sharing a canonical name: an alias that another command
|
|
47
|
+
* already published (or already claims as its alias) is the same kind of
|
|
48
|
+
* claim, and registration order decides it — the later registration owns the
|
|
49
|
+
* token and the earlier one stops advertising it. The earlier command keeps
|
|
50
|
+
* its canonical name, so nothing becomes unreachable.
|
|
51
|
+
*/
|
|
52
|
+
register(child) {
|
|
53
|
+
const index = this.children.findIndex((existing) => existing.name === child.name
|
|
54
|
+
|| existing.aliases.includes(child.name)
|
|
55
|
+
|| child.aliases.includes(existing.name));
|
|
56
|
+
if (index === -1) {
|
|
57
|
+
for (const sibling of this.children) {
|
|
58
|
+
sibling.aliases = sibling.aliases.filter((alias) => !child.aliases.includes(alias));
|
|
59
|
+
}
|
|
60
|
+
this.children.push(child);
|
|
61
|
+
return child;
|
|
62
|
+
}
|
|
63
|
+
// Carry the replaced node's aliases over — minus any alias that is now the
|
|
64
|
+
// replacement's own canonical name, which would advertise the command as
|
|
65
|
+
// its own alias.
|
|
66
|
+
child.aliases = [...new Set([...child.aliases, ...this.children[index].aliases])]
|
|
67
|
+
.filter((alias) => alias !== child.name);
|
|
68
|
+
for (const sibling of this.children) {
|
|
69
|
+
if (sibling === this.children[index]) continue;
|
|
70
|
+
sibling.aliases = sibling.aliases.filter((alias) => !child.aliases.includes(alias));
|
|
71
|
+
}
|
|
72
|
+
this.children[index] = child;
|
|
73
|
+
return child;
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
api() {
|
|
28
77
|
const node = this;
|
|
29
78
|
return {
|
|
30
79
|
command(definition, builder, handler) {
|
|
31
|
-
node.
|
|
80
|
+
node.register(new CommandNode(typeof definition === 'string' ? { command: definition, builder, handler } : definition));
|
|
32
81
|
return this;
|
|
33
82
|
},
|
|
34
83
|
option(name, config = {}) { node.options.set(name, { ...config, name }); return this; },
|
|
@@ -63,10 +112,16 @@ export class LocalCLI {
|
|
|
63
112
|
option(name, config) { this.root.options.set(name, { ...config, name }); return this; }
|
|
64
113
|
options(config) { for (const [name, value] of Object.entries(config)) this.option(name, value); return this; }
|
|
65
114
|
command(definition, builder, handler) {
|
|
66
|
-
|
|
67
|
-
this.root.children.push(node);
|
|
115
|
+
this.root.register(new CommandNode(typeof definition === 'string' ? { command: definition, builder, handler } : definition));
|
|
68
116
|
return this;
|
|
69
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Register the fallback used when a parent command (a node with children) is
|
|
120
|
+
* invoked with no subcommand and declares no handler of its own. Without it
|
|
121
|
+
* those parents exited 0 having printed nothing, which reads as success to a
|
|
122
|
+
* script and as a hang to a human.
|
|
123
|
+
*/
|
|
124
|
+
onParentDiscovery(fn) { this.parentDiscovery = fn; return this; }
|
|
70
125
|
demandCommand(count, message) { this.root.demand = { count, message }; return this; }
|
|
71
126
|
completion(name, _description, handler) { this.completionCommand = name; this.completionHandler = handler; return this; }
|
|
72
127
|
help() { this.helpEnabled = true; return this; }
|
|
@@ -124,14 +179,22 @@ export class LocalCLI {
|
|
|
124
179
|
return options.get(key) || [...options.values()].find((spec) => spec.name === key || names(spec.alias).includes(key))
|
|
125
180
|
|| { name: key, type: typeof value === 'boolean' ? 'boolean' : 'string' };
|
|
126
181
|
}
|
|
127
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Assign one parsed value to argv.
|
|
184
|
+
*
|
|
185
|
+
* `validate: false` (tolerant mode, used by parseGlobalOptions) skips the
|
|
186
|
+
* choice/number checks so a value that will be thrown away anyway cannot
|
|
187
|
+
* abort a parse whose only purpose is reading the global output flags.
|
|
188
|
+
*/
|
|
189
|
+
_assign(argv, key, value, spec, opts = {}) {
|
|
190
|
+
const validate = opts.validate !== false;
|
|
128
191
|
const actual = spec?.name || key;
|
|
129
192
|
const values = spec?.type === 'array' ? (Array.isArray(value) ? value : [value]) : [value];
|
|
130
|
-
if (spec?.choices && values.some((item) => !spec.choices.includes(item))) {
|
|
193
|
+
if (validate && spec?.choices && values.some((item) => !spec.choices.includes(item))) {
|
|
131
194
|
this._fail(`Invalid value for --${actual}: ${value}. Choose from: ${spec.choices.join(', ')}`);
|
|
132
195
|
return false;
|
|
133
196
|
}
|
|
134
|
-
if (spec?.type === 'number' && values.some((item) => item === '' || !Number.isFinite(Number(item)))) {
|
|
197
|
+
if (validate && spec?.type === 'number' && values.some((item) => item === '' || !Number.isFinite(Number(item)))) {
|
|
135
198
|
this._fail(`Invalid number for --${actual}: ${value}`);
|
|
136
199
|
return false;
|
|
137
200
|
}
|
|
@@ -143,7 +206,7 @@ export class LocalCLI {
|
|
|
143
206
|
argv[camel(actual)] = argv[actual];
|
|
144
207
|
for (const alias of names(spec?.alias)) argv[alias] = argv[actual];
|
|
145
208
|
}
|
|
146
|
-
_parseOptions(args, path) {
|
|
209
|
+
_parseOptions(args, path, opts = {}) {
|
|
147
210
|
const options = this._optionConfig(path);
|
|
148
211
|
const defs = {};
|
|
149
212
|
for (const [key, spec] of options) defs[key] = {
|
|
@@ -166,15 +229,92 @@ export class LocalCLI {
|
|
|
166
229
|
const argv = { _: path.map((n) => n.name) };
|
|
167
230
|
for (const [key, value] of Object.entries(parsed.values)) {
|
|
168
231
|
const spec = this._specFor(options, key, value);
|
|
169
|
-
if (this._assign(argv, key, value, spec) === false) return null;
|
|
232
|
+
if (this._assign(argv, key, value, spec, opts) === false) return null;
|
|
170
233
|
}
|
|
171
|
-
for (const [key, spec] of options) if (argv[spec.name] === undefined && spec.default !== undefined) this._assign(argv, key, spec.default, spec);
|
|
234
|
+
for (const [key, spec] of options) if (argv[spec.name] === undefined && spec.default !== undefined) this._assign(argv, key, spec.default, spec, opts);
|
|
172
235
|
return { values: argv, positionals: parsed.positionals };
|
|
173
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the global options from raw tokens WITHOUT dispatching a command and
|
|
240
|
+
* without failing.
|
|
241
|
+
*
|
|
242
|
+
* The format middleware never runs when parsing fails, so the parse-time
|
|
243
|
+
* failure renderer has to know the format the caller asked for. Rather than
|
|
244
|
+
* re-scanning argv with a second, approximate tokenizer (any `--json`
|
|
245
|
+
* anywhere, first `--format` wins, no option termination, no value
|
|
246
|
+
* consumption, no `--no-` negation), it asks the SAME parser the successful
|
|
247
|
+
* path uses. Tolerant mode keeps a value error (`--format xml`, the very
|
|
248
|
+
* failure being rendered) from aborting the resolution.
|
|
249
|
+
*
|
|
250
|
+
* Returns null when the tokens cannot be parsed at all — the caller then
|
|
251
|
+
* falls back to the pipe/TTY default, exactly as it did before.
|
|
252
|
+
*/
|
|
253
|
+
parseGlobalOptions(args) {
|
|
254
|
+
const saved = this.failHandler;
|
|
255
|
+
this.failHandler = null; // errors here are not the failure being reported
|
|
256
|
+
try {
|
|
257
|
+
const { path, commandArgs } = this._findPath(args);
|
|
258
|
+
const parsed = this._parseOptions(commandArgs, path, { validate: false });
|
|
259
|
+
return parsed ? parsed.values : null;
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
} finally {
|
|
263
|
+
this.failHandler = saved;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
174
266
|
_fail(message, error) {
|
|
175
267
|
if (this.failHandler) return this.failHandler(message, error);
|
|
176
268
|
throw error || new Error(message);
|
|
177
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Describe the registered command surface: root commands (canonical name,
|
|
272
|
+
* aliases, describe, child names) and the global option declarations.
|
|
273
|
+
*
|
|
274
|
+
* This is the single source both the grouped root help and the machine
|
|
275
|
+
* discovery output are generated from, so neither can drift away from the
|
|
276
|
+
* commands that actually exist. Builders run here (idempotently) because a
|
|
277
|
+
* parent's children are only registered when its builder is invoked.
|
|
278
|
+
*/
|
|
279
|
+
commandRegistry() {
|
|
280
|
+
const visible = (node) => node.name !== this.completionCommand;
|
|
281
|
+
const serialize = (node) => {
|
|
282
|
+
node.build();
|
|
283
|
+
return {
|
|
284
|
+
name: node.name,
|
|
285
|
+
command: node.definition.command || node.name,
|
|
286
|
+
aliases: [...node.aliases],
|
|
287
|
+
description: node.definition.describe || '',
|
|
288
|
+
subcommands: node.children.filter(visible).map((child) => child.name),
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
commands: this.root.children.filter(visible).map(serialize),
|
|
293
|
+
options: [...this.root.options.values()].map((spec) => ({
|
|
294
|
+
name: spec.name,
|
|
295
|
+
alias: names(spec.alias),
|
|
296
|
+
description: spec.description || spec.describe || '',
|
|
297
|
+
choices: spec.choices || [],
|
|
298
|
+
type: spec.type || 'boolean',
|
|
299
|
+
})),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Structured child signatures for a parent node, built on demand. */
|
|
304
|
+
subcommandsOf(node) {
|
|
305
|
+
return node.children
|
|
306
|
+
.filter((child) => child.name !== this.completionCommand)
|
|
307
|
+
.map((child) => {
|
|
308
|
+
child.build();
|
|
309
|
+
return {
|
|
310
|
+
name: child.name,
|
|
311
|
+
command: child.definition.command || child.name,
|
|
312
|
+
aliases: [...child.aliases],
|
|
313
|
+
description: child.definition.describe || '',
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
178
318
|
_help(node, path = []) {
|
|
179
319
|
const lines = [node.usageText || `Usage: ${this.script} ${path.join(' ') || '<command>'}`];
|
|
180
320
|
if (node.definition.describe) lines.push(`\n${node.definition.describe}`);
|
|
@@ -232,6 +372,11 @@ export class LocalCLI {
|
|
|
232
372
|
const parsed = this._parseOptions(commandArgs, path);
|
|
233
373
|
if (!parsed) return;
|
|
234
374
|
const argv = parsed.values;
|
|
375
|
+
// A parent command's own children are registry-derived, so a default
|
|
376
|
+
// handler can list exactly what is registered instead of repeating a
|
|
377
|
+
// hand-written list that drifts (see the records parent, which advertised
|
|
378
|
+
// an `inspect` description the registration disagreed with).
|
|
379
|
+
if (node.children.length) argv.subcommands = this.subcommandsOf(node);
|
|
235
380
|
const specs = [...this._optionConfig(path).values()];
|
|
236
381
|
const positions = positionSpec(node.definition.command || '').map((position) => ({
|
|
237
382
|
...position, ...node.positionals.get(position.name), name: position.name,
|
|
@@ -242,7 +387,13 @@ export class LocalCLI {
|
|
|
242
387
|
for (const middleware of this.middlewares) await middleware(argv);
|
|
243
388
|
for (const commandNode of path) for (const middleware of commandNode.middlewares) await middleware(argv);
|
|
244
389
|
if (node.definition.handler) await node.definition.handler(argv);
|
|
245
|
-
|
|
390
|
+
// A parent with no handler of its own used to fall through and exit 0
|
|
391
|
+
// with no output — indistinguishable from a command that ran and found
|
|
392
|
+
// nothing. The registered discovery hook (cli.js) prints the child
|
|
393
|
+
// surface instead: a JSON envelope in machine mode, a list for humans.
|
|
394
|
+
else if (path.length && argv.subcommands?.length && this.parentDiscovery) {
|
|
395
|
+
await this.parentDiscovery(argv, { path: path.map((n) => n.name), subcommands: argv.subcommands });
|
|
396
|
+
} else if (node.demand?.count && !path.length) return this._fail(node.demand.message);
|
|
246
397
|
}
|
|
247
398
|
}
|
|
248
399
|
|
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
|
+
};
|