@jacebenson/jsn 4.0.4 → 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 -3
- package/src/app.js +48 -64
- package/src/auth.js +46 -11
- package/src/capabilities.js +128 -0
- package/src/cli.js +129 -65
- package/src/commands/_ticket.js +37 -93
- 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 +25 -81
- package/src/commands/cmdb.js +613 -0
- package/src/commands/codesearch.js +103 -0
- package/src/commands/dev/_generic.js +63 -111
- 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 +65 -7
- 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 +274 -12
- 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 +107 -1
- package/src/help.js +1 -0
- package/src/helpers.js +131 -15
- package/src/mutations.js +36 -91
- package/src/output.js +195 -64
- 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,16 +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 {
|
|
12
|
+
import { isMutationCommand, refreshMutationCommands } from './mutations.js';
|
|
13
|
+
import { noInstanceCommands, dailyCheckSkipCommands } from './capabilities.js';
|
|
14
|
+
import { guardExit, exitWithError } from './errors.js';
|
|
13
15
|
|
|
14
16
|
// Command modules
|
|
15
17
|
import { setupCmd } from './commands/setup.js';
|
|
16
18
|
import { authCmd } from './commands/auth.js';
|
|
17
19
|
import { recordsCmd } from './commands/records.js';
|
|
20
|
+
import { cmdbCmd } from './commands/cmdb.js';
|
|
21
|
+
import { snippetsCmd } from './commands/snippets.js';
|
|
18
22
|
import { catalogCmd } from './commands/catalog.js';
|
|
19
23
|
import { incidentsCmd } from './commands/incidents.js';
|
|
20
24
|
import { changesCmd } from './commands/changes.js';
|
|
@@ -25,6 +29,7 @@ import { groupsCmd } from './commands/groups.js';
|
|
|
25
29
|
import { groupMembersCmd } from './commands/groupmembers.js';
|
|
26
30
|
import { groupRolesCmd } from './commands/grouproles.js';
|
|
27
31
|
import { ticketsCmd } from './commands/tickets.js';
|
|
32
|
+
import { codesearchCmd } from './commands/codesearch.js';
|
|
28
33
|
import { versionCmd } from './commands/version.js';
|
|
29
34
|
import { devCmd } from './commands/dev.js';
|
|
30
35
|
import { skillCmd, checkSkill } from './commands/skill.js';
|
|
@@ -34,7 +39,7 @@ import { docsCmd } from './commands/docs/docs.js';
|
|
|
34
39
|
// Dev subcommands promoted to root level for progressive disclosure
|
|
35
40
|
import {
|
|
36
41
|
actionsCmd, includesCmd, rulesCmd,
|
|
37
|
-
clientScriptsCmd, uiActionsCmd,
|
|
42
|
+
clientScriptsCmd, uiActionsCmd,
|
|
38
43
|
tablesCmd, columnsCmd, importCmd,
|
|
39
44
|
spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
|
|
40
45
|
aclsCmd, rolesCmd, propertiesCmd,
|
|
@@ -59,6 +64,8 @@ import { restCmd } from './commands/dev/rest.js';
|
|
|
59
64
|
import { logsCmd } from './commands/dev/logs.js';
|
|
60
65
|
import { scrapiCmd } from './commands/dev/scrapi.js';
|
|
61
66
|
import { b4rulesCmd } from './commands/dev/b4rules.js';
|
|
67
|
+
import { atfCmd } from './commands/atf.js';
|
|
68
|
+
import { approvalsCmd } from './commands/approvals.js';
|
|
62
69
|
|
|
63
70
|
function wrap(handler) {
|
|
64
71
|
return async (argv) => {
|
|
@@ -70,24 +77,11 @@ function wrap(handler) {
|
|
|
70
77
|
}
|
|
71
78
|
await handler(argv, app);
|
|
72
79
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
if (err.code === 'usage') {
|
|
82
|
-
process.stderr.write(`Error (usage): ${err.message}\n`);
|
|
83
|
-
process.exit(2);
|
|
84
|
-
}
|
|
85
|
-
if (err.code === 'system_error') {
|
|
86
|
-
process.stderr.write(`Error (system): ${err.message}\n`);
|
|
87
|
-
process.exit(3);
|
|
88
|
-
}
|
|
89
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
90
|
-
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 });
|
|
91
85
|
}
|
|
92
86
|
};
|
|
93
87
|
}
|
|
@@ -95,15 +89,17 @@ function wrap(handler) {
|
|
|
95
89
|
export function buildCLI() {
|
|
96
90
|
const cfg = loadConfig();
|
|
97
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 = [];
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
const cliInstance = yargs(hideBin(process.argv))
|
|
100
97
|
.scriptName('jsn')
|
|
101
98
|
.usage('Usage: jsn <command> [options]')
|
|
102
99
|
.middleware([
|
|
103
100
|
// Attach App instance to every command
|
|
104
101
|
(argv) => {
|
|
105
102
|
argv.app = app;
|
|
106
|
-
argv._profile = cfg.profiles || {};
|
|
107
103
|
},
|
|
108
104
|
])
|
|
109
105
|
.options({
|
|
@@ -119,13 +115,21 @@ export function buildCLI() {
|
|
|
119
115
|
},
|
|
120
116
|
format: {
|
|
121
117
|
type: 'string',
|
|
122
|
-
description: 'Output format: auto, json, markdown, styled, quiet',
|
|
123
|
-
choices: ['auto', 'json', 'markdown', 'styled', 'quiet'],
|
|
118
|
+
description: 'Output format: auto, json, markdown, styled, quiet, csv',
|
|
119
|
+
choices: ['auto', 'json', 'markdown', 'styled', 'quiet', 'csv'],
|
|
124
120
|
},
|
|
125
121
|
json: {
|
|
126
122
|
type: 'boolean',
|
|
127
123
|
description: 'Output in JSON format (shortcut for --format=json)',
|
|
128
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
|
+
},
|
|
129
133
|
quiet: {
|
|
130
134
|
type: 'boolean',
|
|
131
135
|
description: 'Output only data, no envelope (shortcut for --format=quiet)',
|
|
@@ -147,42 +151,37 @@ export function buildCLI() {
|
|
|
147
151
|
if (argv.quiet) argv.format = 'quiet';
|
|
148
152
|
if (argv.styled) argv.format = 'styled';
|
|
149
153
|
if (argv.markdown) argv.format = 'markdown';
|
|
154
|
+
if (argv.csv) argv.format = 'csv';
|
|
150
155
|
if (argv.format) {
|
|
151
156
|
app.output.setFormat(argv.format);
|
|
152
157
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (argv.profile) {
|
|
156
|
-
const profile = (cfg.profiles || {})[argv.profile];
|
|
157
|
-
if (!profile) {
|
|
158
|
-
guardExit(argv, {
|
|
159
|
-
code: 'unknown_profile',
|
|
160
|
-
message: `Unknown profile "${argv.profile}"`,
|
|
161
|
-
hint: 'Run "jsn auth status" to list profiles.',
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
const url = profile.instance_url;
|
|
165
|
-
app._overrideInstance = url;
|
|
166
|
-
argv._overrideInstance = url;
|
|
167
|
-
// Temporarily switch active profile so auth picks up the right user
|
|
168
|
-
cfg.activeProfile = argv.profile;
|
|
158
|
+
if (argv.get) {
|
|
159
|
+
app.output.setJqFilter(argv.get);
|
|
169
160
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
});
|
|
173
174
|
}
|
|
175
|
+
applySession(app, session);
|
|
174
176
|
},
|
|
175
177
|
// Guard mutation commands: require an instance, block read-only profiles
|
|
176
178
|
(argv) => {
|
|
177
179
|
const cmd = (argv._[0] || '').toString();
|
|
178
|
-
|
|
180
|
+
// Skip-list is derived from command capability declarations
|
|
181
|
+
// (noInstance: true), not a hand-maintained name list.
|
|
182
|
+
if (!cmd || noInstanceCommands().has(cmd)) {
|
|
179
183
|
return;
|
|
180
184
|
}
|
|
181
|
-
// Check for --instance override
|
|
182
|
-
if (argv.instance) {
|
|
183
|
-
app._overrideInstance = argv.instance;
|
|
184
|
-
argv._overrideInstance = argv.instance;
|
|
185
|
-
}
|
|
186
185
|
if (isMutationCommand(argv)) {
|
|
187
186
|
try {
|
|
188
187
|
app.requireInstance();
|
|
@@ -192,12 +191,10 @@ export function buildCLI() {
|
|
|
192
191
|
// raw stack trace).
|
|
193
192
|
guardExit(argv, { code: err.code || 'usage', message: err.message, hint: err.hint });
|
|
194
193
|
}
|
|
195
|
-
|
|
196
|
-
if (profile?.read_only === true) {
|
|
197
|
-
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
194
|
+
if (app.session?.readOnly === true) {
|
|
198
195
|
guardExit(argv, {
|
|
199
196
|
code: 'read_only',
|
|
200
|
-
message: `Profile "${
|
|
197
|
+
message: `Profile "${app.session.profileName}" is read-only. Mutations are blocked.`,
|
|
201
198
|
hint: 'Switch to a write-enabled profile first:\n jsn auth switch <name>',
|
|
202
199
|
});
|
|
203
200
|
}
|
|
@@ -205,22 +202,22 @@ export function buildCLI() {
|
|
|
205
202
|
},
|
|
206
203
|
])
|
|
207
204
|
.middleware(async (argv) => {
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
app.setEffectiveInstance(argv._overrideInstance);
|
|
211
|
-
}
|
|
212
|
-
|
|
205
|
+
// Session (incl. --instance/--profile overrides) was already applied
|
|
206
|
+
// by the middleware above — nothing to re-apply here.
|
|
213
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();
|
|
214
211
|
|
|
215
212
|
// Daily npm version check (fire-and-forget, non-blocking)
|
|
216
213
|
// Checks npm for newer jsn releases, at most once per 24 hours.
|
|
217
|
-
const skipNpmCheck =
|
|
214
|
+
const skipNpmCheck = dailySkips.has(cmd)
|
|
218
215
|
|| process.env.JSN_NO_VERSION_CHECK === '1'
|
|
219
216
|
|| argv.json
|
|
220
217
|
|| argv.quiet;
|
|
221
218
|
|
|
222
219
|
// Auto-check skill on every command (once per 24h, non-blocking)
|
|
223
|
-
const skipSkillCheck =
|
|
220
|
+
const skipSkillCheck = dailySkips.has(cmd)
|
|
224
221
|
|| process.env.JSN_NO_SKILL_CHECK === '1'
|
|
225
222
|
|| argv['no-skill-check'];
|
|
226
223
|
if (!skipSkillCheck) {
|
|
@@ -279,7 +276,7 @@ export function buildCLI() {
|
|
|
279
276
|
}
|
|
280
277
|
|
|
281
278
|
// Print context header for interactive terminals
|
|
282
|
-
if (!
|
|
279
|
+
if (!dailySkips.has(cmd)) {
|
|
283
280
|
await argv.app.printContextHeader(argv);
|
|
284
281
|
}
|
|
285
282
|
})
|
|
@@ -326,11 +323,10 @@ export function buildCLI() {
|
|
|
326
323
|
.command(formsCmd(wrap))
|
|
327
324
|
.command(listsCmd(wrap))
|
|
328
325
|
.command(clientScriptsCmd(wrap))
|
|
329
|
-
.command(
|
|
326
|
+
.command(uipoliciesCmd(wrap))
|
|
330
327
|
.command(uiActionsCmd(wrap))
|
|
331
328
|
.command(viewsCmd(wrap))
|
|
332
329
|
.command(catalogscriptsCmd(wrap))
|
|
333
|
-
.command(uipoliciesCmd(wrap))
|
|
334
330
|
.command(cataloguipoliciesCmd(wrap))
|
|
335
331
|
// USER EXPERIENCE — Core UI
|
|
336
332
|
.command(uiPagesCmd(wrap))
|
|
@@ -356,9 +352,14 @@ export function buildCLI() {
|
|
|
356
352
|
.command(logsCmd(wrap))
|
|
357
353
|
.command(propertiesCmd(wrap))
|
|
358
354
|
// DATA
|
|
355
|
+
.command(cmdbCmd(wrap))
|
|
359
356
|
.command(recordsCmd(wrap))
|
|
357
|
+
.command(snippetsCmd(wrap))
|
|
360
358
|
.command(ticketsCmd(wrap))
|
|
359
|
+
.command(codesearchCmd(wrap))
|
|
361
360
|
// DEVELOPER
|
|
361
|
+
.command(atfCmd(wrap))
|
|
362
|
+
.command(approvalsCmd(wrap))
|
|
362
363
|
.command(evalCmd(wrap))
|
|
363
364
|
.command(restCmd(wrap))
|
|
364
365
|
.command(skillCmd(wrap))
|
|
@@ -367,6 +368,59 @@ export function buildCLI() {
|
|
|
367
368
|
// Legacy
|
|
368
369
|
.command(devCmd(wrap))
|
|
369
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
|
+
})
|
|
370
424
|
.help('help', 'Show help')
|
|
371
425
|
.version(false)
|
|
372
426
|
.strictCommands()
|
|
@@ -396,6 +450,16 @@ export function buildCLI() {
|
|
|
396
450
|
process.stderr.write(msg + '\n');
|
|
397
451
|
process.exit(1);
|
|
398
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;
|
|
399
463
|
}
|
|
400
464
|
|
|
401
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 {
|
|
6
|
-
import {
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, interactiveList, canPrompt } from '../helpers.js';
|
|
5
|
+
import { resolveRecord, resolveSysId } from '../resolve-record.js';
|
|
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}`,
|