@jacebenson/jsn 4.1.0 → 4.2.0
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 +48 -64
- 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/cli.js
CHANGED
|
@@ -5,17 +5,20 @@ import { hideBin } from 'yargs/helpers';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
|
-
import { loadConfig,
|
|
8
|
+
import { loadConfig, globalConfigDir } from './config.js';
|
|
9
9
|
import { App } from './app.js';
|
|
10
|
+
import { resolveSession, applySession } from './session.js';
|
|
10
11
|
import { renderHelp } from './help.js';
|
|
11
|
-
import { isMutationCommand } from './mutations.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { isMutationCommand, refreshMutationCommands } from './mutations.js';
|
|
13
|
+
import { noInstanceCommands, dailyCheckSkipCommands } from './capabilities.js';
|
|
14
|
+
import { guardExit, exitWithError } from './errors.js';
|
|
14
15
|
|
|
15
16
|
// Command modules
|
|
16
17
|
import { setupCmd } from './commands/setup.js';
|
|
17
18
|
import { authCmd } from './commands/auth.js';
|
|
18
19
|
import { recordsCmd } from './commands/records.js';
|
|
20
|
+
import { cmdbCmd } from './commands/cmdb.js';
|
|
21
|
+
import { snippetsCmd } from './commands/snippets.js';
|
|
19
22
|
import { catalogCmd } from './commands/catalog.js';
|
|
20
23
|
import { incidentsCmd } from './commands/incidents.js';
|
|
21
24
|
import { changesCmd } from './commands/changes.js';
|
|
@@ -26,6 +29,7 @@ import { groupsCmd } from './commands/groups.js';
|
|
|
26
29
|
import { groupMembersCmd } from './commands/groupmembers.js';
|
|
27
30
|
import { groupRolesCmd } from './commands/grouproles.js';
|
|
28
31
|
import { ticketsCmd } from './commands/tickets.js';
|
|
32
|
+
import { codesearchCmd } from './commands/codesearch.js';
|
|
29
33
|
import { versionCmd } from './commands/version.js';
|
|
30
34
|
import { devCmd } from './commands/dev.js';
|
|
31
35
|
import { skillCmd, checkSkill } from './commands/skill.js';
|
|
@@ -35,7 +39,7 @@ import { docsCmd } from './commands/docs/docs.js';
|
|
|
35
39
|
// Dev subcommands promoted to root level for progressive disclosure
|
|
36
40
|
import {
|
|
37
41
|
actionsCmd, includesCmd, rulesCmd,
|
|
38
|
-
clientScriptsCmd, uiActionsCmd,
|
|
42
|
+
clientScriptsCmd, uiActionsCmd,
|
|
39
43
|
tablesCmd, columnsCmd, importCmd,
|
|
40
44
|
spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
|
|
41
45
|
aclsCmd, rolesCmd, propertiesCmd,
|
|
@@ -60,6 +64,8 @@ import { restCmd } from './commands/dev/rest.js';
|
|
|
60
64
|
import { logsCmd } from './commands/dev/logs.js';
|
|
61
65
|
import { scrapiCmd } from './commands/dev/scrapi.js';
|
|
62
66
|
import { b4rulesCmd } from './commands/dev/b4rules.js';
|
|
67
|
+
import { atfCmd } from './commands/atf.js';
|
|
68
|
+
import { approvalsCmd } from './commands/approvals.js';
|
|
63
69
|
|
|
64
70
|
function wrap(handler) {
|
|
65
71
|
return async (argv) => {
|
|
@@ -71,37 +77,11 @@ function wrap(handler) {
|
|
|
71
77
|
}
|
|
72
78
|
await handler(argv, app);
|
|
73
79
|
} catch (err) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
if (err.code === 'usage') {
|
|
83
|
-
process.stderr.write(`Error (usage): ${err.message}\n`);
|
|
84
|
-
process.exit(2);
|
|
85
|
-
}
|
|
86
|
-
if (err.code === 'system_error') {
|
|
87
|
-
process.stderr.write(`Error (system): ${err.message}\n`);
|
|
88
|
-
process.exit(3);
|
|
89
|
-
}
|
|
90
|
-
if (err.code === 'confirmation_required') {
|
|
91
|
-
// Structured error for AI agents / piped consumers: emit the
|
|
92
|
-
// JSON envelope on stdout (where tooling reads) so the agent can
|
|
93
|
-
// surface the decision to its user or re-run with --force.
|
|
94
|
-
const app = argv.app;
|
|
95
|
-
if (app && app.output.effectiveFormat() === FormatJSON) {
|
|
96
|
-
app.err(err);
|
|
97
|
-
} else {
|
|
98
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
99
|
-
if (err.hint) process.stderr.write(`\n${err.hint}\n`);
|
|
100
|
-
}
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
104
|
-
process.exit(1);
|
|
80
|
+
// All error rendering goes through the unified renderer in
|
|
81
|
+
// src/errors.js — exit codes, stream choice, and the JSON envelope
|
|
82
|
+
// for confirmation_required live there, not here.
|
|
83
|
+
const identifier = argv._.slice(1).join(' ') || argv.id || argv.name || argv.sysID || '';
|
|
84
|
+
exitWithError(err, { app: argv.app, argv, identifier });
|
|
105
85
|
}
|
|
106
86
|
};
|
|
107
87
|
}
|
|
@@ -109,15 +89,17 @@ function wrap(handler) {
|
|
|
109
89
|
export function buildCLI() {
|
|
110
90
|
const cfg = loadConfig();
|
|
111
91
|
const app = new App(cfg);
|
|
92
|
+
// Root command names for the completion filter — captured from the yargs
|
|
93
|
+
// registry after the chain below finishes registering all commands.
|
|
94
|
+
let rootCommands = [];
|
|
112
95
|
|
|
113
|
-
|
|
96
|
+
const cliInstance = yargs(hideBin(process.argv))
|
|
114
97
|
.scriptName('jsn')
|
|
115
98
|
.usage('Usage: jsn <command> [options]')
|
|
116
99
|
.middleware([
|
|
117
100
|
// Attach App instance to every command
|
|
118
101
|
(argv) => {
|
|
119
102
|
argv.app = app;
|
|
120
|
-
argv._profile = cfg.profiles || {};
|
|
121
103
|
},
|
|
122
104
|
])
|
|
123
105
|
.options({
|
|
@@ -133,13 +115,21 @@ export function buildCLI() {
|
|
|
133
115
|
},
|
|
134
116
|
format: {
|
|
135
117
|
type: 'string',
|
|
136
|
-
description: 'Output format: auto, json, markdown, styled, quiet',
|
|
137
|
-
choices: ['auto', 'json', 'markdown', 'styled', 'quiet'],
|
|
118
|
+
description: 'Output format: auto, json, markdown, styled, quiet, csv',
|
|
119
|
+
choices: ['auto', 'json', 'markdown', 'styled', 'quiet', 'csv'],
|
|
138
120
|
},
|
|
139
121
|
json: {
|
|
140
122
|
type: 'boolean',
|
|
141
123
|
description: 'Output in JSON format (shortcut for --format=json)',
|
|
142
124
|
},
|
|
125
|
+
csv: {
|
|
126
|
+
type: 'boolean',
|
|
127
|
+
description: 'Output in CSV format (shortcut for --format=csv)',
|
|
128
|
+
},
|
|
129
|
+
get: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Extract a JSON path from the output envelope (e.g. --get "data.records.0.number"); no jq needed',
|
|
132
|
+
},
|
|
143
133
|
quiet: {
|
|
144
134
|
type: 'boolean',
|
|
145
135
|
description: 'Output only data, no envelope (shortcut for --format=quiet)',
|
|
@@ -161,42 +151,37 @@ export function buildCLI() {
|
|
|
161
151
|
if (argv.quiet) argv.format = 'quiet';
|
|
162
152
|
if (argv.styled) argv.format = 'styled';
|
|
163
153
|
if (argv.markdown) argv.format = 'markdown';
|
|
154
|
+
if (argv.csv) argv.format = 'csv';
|
|
164
155
|
if (argv.format) {
|
|
165
156
|
app.output.setFormat(argv.format);
|
|
166
157
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (argv.profile) {
|
|
170
|
-
const profile = (cfg.profiles || {})[argv.profile];
|
|
171
|
-
if (!profile) {
|
|
172
|
-
guardExit(argv, {
|
|
173
|
-
code: 'unknown_profile',
|
|
174
|
-
message: `Unknown profile "${argv.profile}"`,
|
|
175
|
-
hint: 'Run "jsn auth status" to list profiles.',
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
const url = profile.instance_url;
|
|
179
|
-
app._overrideInstance = url;
|
|
180
|
-
argv._overrideInstance = url;
|
|
181
|
-
// Temporarily switch active profile so auth picks up the right user
|
|
182
|
-
cfg.activeProfile = argv.profile;
|
|
158
|
+
if (argv.get) {
|
|
159
|
+
app.output.setJqFilter(argv.get);
|
|
183
160
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
161
|
+
},
|
|
162
|
+
// --profile / --instance resolve through the SESSION (src/session.js).
|
|
163
|
+
// resolveSession is pure; applySession is the ONLY place cfg.activeProfile
|
|
164
|
+
// / app._overrideInstance change — no more argv._overrideInstance mirror
|
|
165
|
+
// or duplicated --instance branches across middlewares.
|
|
166
|
+
(argv) => {
|
|
167
|
+
const session = resolveSession(argv, cfg);
|
|
168
|
+
if (session.unknownProfile) {
|
|
169
|
+
guardExit(argv, {
|
|
170
|
+
code: 'unknown_profile',
|
|
171
|
+
message: `Unknown profile "${session.unknownProfile}"`,
|
|
172
|
+
hint: 'Run "jsn auth status" to list profiles.',
|
|
173
|
+
});
|
|
187
174
|
}
|
|
175
|
+
applySession(app, session);
|
|
188
176
|
},
|
|
189
177
|
// Guard mutation commands: require an instance, block read-only profiles
|
|
190
178
|
(argv) => {
|
|
191
179
|
const cmd = (argv._[0] || '').toString();
|
|
192
|
-
|
|
180
|
+
// Skip-list is derived from command capability declarations
|
|
181
|
+
// (noInstance: true), not a hand-maintained name list.
|
|
182
|
+
if (!cmd || noInstanceCommands().has(cmd)) {
|
|
193
183
|
return;
|
|
194
184
|
}
|
|
195
|
-
// Check for --instance override
|
|
196
|
-
if (argv.instance) {
|
|
197
|
-
app._overrideInstance = argv.instance;
|
|
198
|
-
argv._overrideInstance = argv.instance;
|
|
199
|
-
}
|
|
200
185
|
if (isMutationCommand(argv)) {
|
|
201
186
|
try {
|
|
202
187
|
app.requireInstance();
|
|
@@ -206,12 +191,10 @@ export function buildCLI() {
|
|
|
206
191
|
// raw stack trace).
|
|
207
192
|
guardExit(argv, { code: err.code || 'usage', message: err.message, hint: err.hint });
|
|
208
193
|
}
|
|
209
|
-
|
|
210
|
-
if (profile?.read_only === true) {
|
|
211
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
194
|
+
if (app.session?.readOnly === true) {
|
|
212
195
|
guardExit(argv, {
|
|
213
196
|
code: 'read_only',
|
|
214
|
-
message: `Profile "${
|
|
197
|
+
message: `Profile "${app.session.profileName}" is read-only. Mutations are blocked.`,
|
|
215
198
|
hint: 'Switch to a write-enabled profile first:\n jsn auth switch <name>',
|
|
216
199
|
});
|
|
217
200
|
}
|
|
@@ -219,22 +202,22 @@ export function buildCLI() {
|
|
|
219
202
|
},
|
|
220
203
|
])
|
|
221
204
|
.middleware(async (argv) => {
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
app.setEffectiveInstance(argv._overrideInstance);
|
|
225
|
-
}
|
|
226
|
-
|
|
205
|
+
// Session (incl. --instance/--profile overrides) was already applied
|
|
206
|
+
// by the middleware above — nothing to re-apply here.
|
|
227
207
|
const cmd = (argv._[0] || '').toString();
|
|
208
|
+
// Daily-check + context-header skip-list is derived from command
|
|
209
|
+
// capability declarations (skipDailyChecks: true).
|
|
210
|
+
const dailySkips = dailyCheckSkipCommands();
|
|
228
211
|
|
|
229
212
|
// Daily npm version check (fire-and-forget, non-blocking)
|
|
230
213
|
// Checks npm for newer jsn releases, at most once per 24 hours.
|
|
231
|
-
const skipNpmCheck =
|
|
214
|
+
const skipNpmCheck = dailySkips.has(cmd)
|
|
232
215
|
|| process.env.JSN_NO_VERSION_CHECK === '1'
|
|
233
216
|
|| argv.json
|
|
234
217
|
|| argv.quiet;
|
|
235
218
|
|
|
236
219
|
// Auto-check skill on every command (once per 24h, non-blocking)
|
|
237
|
-
const skipSkillCheck =
|
|
220
|
+
const skipSkillCheck = dailySkips.has(cmd)
|
|
238
221
|
|| process.env.JSN_NO_SKILL_CHECK === '1'
|
|
239
222
|
|| argv['no-skill-check'];
|
|
240
223
|
if (!skipSkillCheck) {
|
|
@@ -293,7 +276,7 @@ export function buildCLI() {
|
|
|
293
276
|
}
|
|
294
277
|
|
|
295
278
|
// Print context header for interactive terminals
|
|
296
|
-
if (!
|
|
279
|
+
if (!dailySkips.has(cmd)) {
|
|
297
280
|
await argv.app.printContextHeader(argv);
|
|
298
281
|
}
|
|
299
282
|
})
|
|
@@ -340,11 +323,10 @@ export function buildCLI() {
|
|
|
340
323
|
.command(formsCmd(wrap))
|
|
341
324
|
.command(listsCmd(wrap))
|
|
342
325
|
.command(clientScriptsCmd(wrap))
|
|
343
|
-
.command(
|
|
326
|
+
.command(uipoliciesCmd(wrap))
|
|
344
327
|
.command(uiActionsCmd(wrap))
|
|
345
328
|
.command(viewsCmd(wrap))
|
|
346
329
|
.command(catalogscriptsCmd(wrap))
|
|
347
|
-
.command(uipoliciesCmd(wrap))
|
|
348
330
|
.command(cataloguipoliciesCmd(wrap))
|
|
349
331
|
// USER EXPERIENCE — Core UI
|
|
350
332
|
.command(uiPagesCmd(wrap))
|
|
@@ -370,9 +352,14 @@ export function buildCLI() {
|
|
|
370
352
|
.command(logsCmd(wrap))
|
|
371
353
|
.command(propertiesCmd(wrap))
|
|
372
354
|
// DATA
|
|
355
|
+
.command(cmdbCmd(wrap))
|
|
373
356
|
.command(recordsCmd(wrap))
|
|
357
|
+
.command(snippetsCmd(wrap))
|
|
374
358
|
.command(ticketsCmd(wrap))
|
|
359
|
+
.command(codesearchCmd(wrap))
|
|
375
360
|
// DEVELOPER
|
|
361
|
+
.command(atfCmd(wrap))
|
|
362
|
+
.command(approvalsCmd(wrap))
|
|
376
363
|
.command(evalCmd(wrap))
|
|
377
364
|
.command(restCmd(wrap))
|
|
378
365
|
.command(skillCmd(wrap))
|
|
@@ -381,6 +368,59 @@ export function buildCLI() {
|
|
|
381
368
|
// Legacy
|
|
382
369
|
.command(devCmd(wrap))
|
|
383
370
|
.demandCommand(1, 'You must specify a command')
|
|
371
|
+
// Completion plumbing under a hidden command name, so the explicit
|
|
372
|
+
// 'completion' command below owns the UX. (#176)
|
|
373
|
+
// The custom filter fixes two yargs behaviors:
|
|
374
|
+
// 1. Under strictCommands(), yargs' default completion handler aborts
|
|
375
|
+
// when the current word isn't an exact command match. Delegating to
|
|
376
|
+
// completionFilter (the default generator) sidesteps that.
|
|
377
|
+
// 2. When the current word is an exact ALIAS (e.g. "inc" for incidents),
|
|
378
|
+
// yargs descends into that command's subcommands, so a still-being-
|
|
379
|
+
// typed "jsn inc<TAB>" offers subcommands that can't match the prefix.
|
|
380
|
+
// When completing the FIRST word (depth 1, the command slot), merge
|
|
381
|
+
// in the root command list — the shell's compgen prefix-filters the
|
|
382
|
+
// final list anyway.
|
|
383
|
+
.completion('__completion', false, (current, argv, completionFilter, done) => {
|
|
384
|
+
completionFilter((err, completions) => {
|
|
385
|
+
let out = completions || [];
|
|
386
|
+
const depth = (argv._ || []).length;
|
|
387
|
+
if (current && !current.startsWith('-') && depth <= 2) {
|
|
388
|
+
out = [...new Set([...out, ...rootCommands])];
|
|
389
|
+
}
|
|
390
|
+
done([...new Set(out)]);
|
|
391
|
+
});
|
|
392
|
+
})
|
|
393
|
+
// User-facing completion command with a real help page (install docs).
|
|
394
|
+
// yargs' built-in completion command prints the script even for --help,
|
|
395
|
+
// which is why the plumbing above uses the hidden '__completion' name.
|
|
396
|
+
.command({
|
|
397
|
+
command: 'completion',
|
|
398
|
+
describe: 'Generate shell completion script',
|
|
399
|
+
builder: (y) => y
|
|
400
|
+
.usage('Usage: jsn completion > <file for your shell>')
|
|
401
|
+
.epilogue([
|
|
402
|
+
'Install for your shell:',
|
|
403
|
+
'',
|
|
404
|
+
' # bash (auto-loaded if the bash-completion package is installed)',
|
|
405
|
+
' jsn completion > ~/.local/share/bash-completion/jsn',
|
|
406
|
+
'',
|
|
407
|
+
' # bash (simple)',
|
|
408
|
+
' jsn completion >> ~/.bashrc',
|
|
409
|
+
'',
|
|
410
|
+
' # fish',
|
|
411
|
+
' jsn completion > ~/.config/fish/completions/jsn.fish',
|
|
412
|
+
'',
|
|
413
|
+
' # zsh — the generated script is bash-style, load via bashcompinit.',
|
|
414
|
+
' # Add to ~/.zshrc:',
|
|
415
|
+
' autoload -U +X bashcompinit && bashcompinit',
|
|
416
|
+
' source <(jsn completion)',
|
|
417
|
+
'',
|
|
418
|
+
'Restart your shell (or source the file) after installing.',
|
|
419
|
+
].join('\n')),
|
|
420
|
+
handler: () => {
|
|
421
|
+
cliInstance.showCompletionScript();
|
|
422
|
+
},
|
|
423
|
+
})
|
|
384
424
|
.help('help', 'Show help')
|
|
385
425
|
.version(false)
|
|
386
426
|
.strictCommands()
|
|
@@ -410,6 +450,16 @@ export function buildCLI() {
|
|
|
410
450
|
process.stderr.write(msg + '\n');
|
|
411
451
|
process.exit(1);
|
|
412
452
|
});
|
|
453
|
+
|
|
454
|
+
// Snapshot the root command names (plus aliases) for the completion
|
|
455
|
+
// filter. Done before any parse — command builders mutate the registry.
|
|
456
|
+
rootCommands = cliInstance.getInternalMethods().getCommandInstance().getCommands();
|
|
457
|
+
|
|
458
|
+
// The capability registry is fully populated now that every command
|
|
459
|
+
// factory has run — generate the mutation guard's path list.
|
|
460
|
+
refreshMutationCommands();
|
|
461
|
+
|
|
462
|
+
return cliInstance;
|
|
413
463
|
}
|
|
414
464
|
|
|
415
465
|
// Run if invoked directly (bin/jsn.js calls cli.parse())
|
package/src/commands/_ticket.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// Generic command builder for CRUD operations on a ServiceNow table
|
|
2
2
|
// Used by incidents, changes, requests, tasks, and most dev subcommands
|
|
3
3
|
|
|
4
|
-
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete,
|
|
5
|
-
import {
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, interactiveList, canPrompt } from '../helpers.js';
|
|
5
|
+
import { resolveRecord, resolveSysId } from '../resolve-record.js';
|
|
6
6
|
import { FormatAuto } from '../output.js';
|
|
7
|
+
import { declareCapabilities } from '../capabilities.js';
|
|
7
8
|
|
|
8
9
|
export function buildTicketCommands(table, displayName, alias, defaultColumns, stateMap, iconFn, wrap) {
|
|
10
|
+
// Ticket-style CRUD: list/show are reads; create/update/delete mutate.
|
|
11
|
+
// Root-level only (no `jsn dev` spelling).
|
|
12
|
+
declareCapabilities(displayName, { mutationSubcommands: ['create', 'update', 'delete'] });
|
|
9
13
|
return {
|
|
10
14
|
command: `${displayName} [subcommand]`,
|
|
11
15
|
aliases: [table, alias],
|
|
@@ -28,18 +32,22 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
28
32
|
const offset = argv.offset;
|
|
29
33
|
|
|
30
34
|
// Interactive picker in TTY with auto format and no explicit query/offset
|
|
31
|
-
|
|
32
|
-
if (isInteractive && app.output.getFormat() === FormatAuto && !query && offset === 0) {
|
|
35
|
+
if (canPrompt() && app.output.getFormat() === FormatAuto && !query && offset === 0) {
|
|
33
36
|
const pickerColumns = ['sys_id', 'number', 'short_description', 'state', 'assigned_to'];
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const picked = await interactiveList({
|
|
38
|
+
app, table, singular: displayName.slice(0, -1), columns: pickerColumns, limit,
|
|
39
|
+
labelField: 'number',
|
|
40
|
+
formatLabel: (r) => {
|
|
41
|
+
let label = `${getStringField(r, 'number')} ${getStringField(r, 'short_description')} | ${getStringField(r, 'state')}`;
|
|
42
|
+
const assigned = getStringField(r, 'assigned_to');
|
|
43
|
+
if (assigned) label += ` → ${assigned}`;
|
|
44
|
+
return label;
|
|
45
|
+
},
|
|
46
|
+
formatValue: (r) => r,
|
|
47
|
+
});
|
|
48
|
+
if (picked === undefined) return; // user cancelled
|
|
49
|
+
if (picked === null) {
|
|
50
|
+
// canPrompt() passed, so null means the table is empty.
|
|
43
51
|
app.ok({
|
|
44
52
|
table,
|
|
45
53
|
count: 0,
|
|
@@ -56,58 +64,19 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
56
64
|
return;
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const record = picked;
|
|
68
|
+
const number = getStringField(record, 'number');
|
|
69
|
+
record._context = {
|
|
70
|
+
instance_url: app.getEffectiveInstance(),
|
|
71
|
+
table,
|
|
72
|
+
};
|
|
73
|
+
app.ok(record, {
|
|
74
|
+
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${number}`,
|
|
75
|
+
breadcrumbs: [
|
|
76
|
+
{ action: 'update', cmd: `jsn ${alias} update ${number} --data '{...}'`, description: `Update this ${displayName}` },
|
|
77
|
+
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
78
|
+
],
|
|
69
79
|
});
|
|
70
|
-
|
|
71
|
-
let selectedNumber;
|
|
72
|
-
try {
|
|
73
|
-
selectedNumber = await search({
|
|
74
|
-
message: `Select a ${displayName.slice(0, -1)}:`,
|
|
75
|
-
source: async (input) => {
|
|
76
|
-
if (!input) return choices;
|
|
77
|
-
const term = input.toLowerCase();
|
|
78
|
-
return choices.filter(c => c.name.toLowerCase().includes(term));
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
} catch (err) {
|
|
82
|
-
if (err.name === 'ExitPromptError' || (err.message && err.message.includes('force closed'))) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
throw err;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (selectedNumber) {
|
|
89
|
-
assertSafeExactMatch(selectedNumber);
|
|
90
|
-
const showParams = new URLSearchParams();
|
|
91
|
-
showParams.set('sysparm_query', `number=${selectedNumber}`);
|
|
92
|
-
showParams.set('sysparm_display_value', 'all');
|
|
93
|
-
showParams.set('sysparm_limit', '1');
|
|
94
|
-
const showRecords = await app.sdk.list(table, showParams);
|
|
95
|
-
if (showRecords.length === 0) {
|
|
96
|
-
throw new Error(`${displayName} not found: ${selectedNumber}`);
|
|
97
|
-
}
|
|
98
|
-
const record = showRecords[0];
|
|
99
|
-
record._context = {
|
|
100
|
-
instance_url: app.getEffectiveInstance(),
|
|
101
|
-
table,
|
|
102
|
-
};
|
|
103
|
-
app.ok(record, {
|
|
104
|
-
summary: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} ${selectedNumber}`,
|
|
105
|
-
breadcrumbs: [
|
|
106
|
-
{ action: 'update', cmd: `jsn ${alias} update ${selectedNumber} --data '{...}'`, description: `Update this ${displayName}` },
|
|
107
|
-
{ action: 'list', cmd: `jsn ${alias} list`, description: `Back to all ${table}` },
|
|
108
|
-
],
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
80
|
return;
|
|
112
81
|
}
|
|
113
82
|
|
|
@@ -162,16 +131,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
162
131
|
describe: `Show a specific ${displayName}`,
|
|
163
132
|
handler: wrap(async (argv, app) => {
|
|
164
133
|
const number = argv.number;
|
|
165
|
-
|
|
166
|
-
const params = new URLSearchParams();
|
|
167
|
-
params.set('sysparm_query', `number=${number}`);
|
|
168
|
-
params.set('sysparm_display_value', 'all');
|
|
169
|
-
params.set('sysparm_limit', '1');
|
|
170
|
-
const records = await app.sdk.list(table, params);
|
|
171
|
-
if (records.length === 0) {
|
|
172
|
-
throw new Error(`${displayName} not found: ${number}`);
|
|
173
|
-
}
|
|
174
|
-
const record = records[0];
|
|
134
|
+
const record = await resolveRecord(app.sdk, { table, identifier: number, matchField: 'number', resource: displayName });
|
|
175
135
|
record._context = {
|
|
176
136
|
instance_url: app.getEffectiveInstance(),
|
|
177
137
|
table,
|
|
@@ -218,16 +178,8 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
218
178
|
.option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
|
|
219
179
|
handler: wrap(async (argv, app) => {
|
|
220
180
|
const number = argv.number;
|
|
221
|
-
assertSafeExactMatch(number);
|
|
222
181
|
const recordData = JSON.parse(argv.data);
|
|
223
|
-
const
|
|
224
|
-
findParams.set('sysparm_query', `number=${number}`);
|
|
225
|
-
findParams.set('sysparm_limit', '1');
|
|
226
|
-
const records = await app.sdk.list(table, findParams);
|
|
227
|
-
if (records.length === 0) {
|
|
228
|
-
throw new Error(`${displayName} not found: ${number}`);
|
|
229
|
-
}
|
|
230
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
182
|
+
const sysID = await resolveSysId(app.sdk, { table, identifier: number, matchField: 'number', resource: displayName });
|
|
231
183
|
const updated = await app.sdk.update(table, sysID, recordData);
|
|
232
184
|
app.ok(updated, {
|
|
233
185
|
summary: `Updated ${displayName} ${number}`,
|
|
@@ -244,15 +196,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
244
196
|
handler: wrap(async (argv, app) => {
|
|
245
197
|
const number = argv.number;
|
|
246
198
|
await confirmDelete(app, argv, `Delete ${displayName} ${number}`);
|
|
247
|
-
|
|
248
|
-
const findParams = new URLSearchParams();
|
|
249
|
-
findParams.set('sysparm_query', `number=${number}`);
|
|
250
|
-
findParams.set('sysparm_limit', '1');
|
|
251
|
-
const records = await app.sdk.list(table, findParams);
|
|
252
|
-
if (records.length === 0) {
|
|
253
|
-
throw new Error(`${displayName} not found: ${number}`);
|
|
254
|
-
}
|
|
255
|
-
const sysID = getStringField(records[0], 'sys_id');
|
|
199
|
+
const sysID = await resolveSysId(app.sdk, { table, identifier: number, matchField: 'number', resource: displayName });
|
|
256
200
|
await app.sdk.delete(table, sysID);
|
|
257
201
|
app.ok({ number, message: `${displayName.charAt(0).toUpperCase() + displayName.slice(1)} deleted` }, {
|
|
258
202
|
summary: `Deleted ${displayName} ${number}`,
|