@jacebenson/jsn 3.4.3 → 4.0.1
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 +5 -4
- package/package.json +3 -1
- package/skills/servicenow/SKILL.md +3 -3
- package/src/app.js +7 -7
- package/src/auth.js +34 -63
- package/src/cli.js +27 -20
- package/src/commands/_ticket.js +7 -1
- package/src/commands/auth.js +303 -40
- package/src/commands/catalog.js +3 -4
- package/src/commands/dev/_generic.js +7 -14
- package/src/commands/dev/flows.js +110 -2
- package/src/commands/dev/forms.js +0 -11
- package/src/commands/dev/import.js +1 -1
- package/src/commands/dev/lists.js +2 -2
- package/src/commands/dev/updatesets.js +0 -16
- package/src/commands/docs/community-sources.json +18 -0
- package/src/commands/docs/community.js +343 -0
- package/src/commands/docs/db.js +38 -2
- package/src/commands/docs/docs.js +41 -0
- package/src/commands/docs/ingest.js +4 -17
- package/src/commands/docs/refresh.js +4 -17
- package/src/commands/docs/search.js +3 -3
- package/src/commands/docs/serve.js +2 -2
- package/src/commands/grouproles.js +5 -1
- package/src/commands/groups.js +6 -3
- package/src/commands/records.js +5 -2
- package/src/commands/requests.js +2 -1
- package/src/commands/setup.js +10 -84
- package/src/commands/skill.js +5 -7
- package/src/commands/tickets.js +4 -1
- package/src/commands/users.js +4 -1
- package/src/config.js +9 -0
- package/src/errors.js +26 -0
- package/src/help.js +2 -4
- package/src/helpers.js +65 -8
- package/src/mutations.js +40 -9
- package/src/output.js +6 -3
- package/src/paginated-search.js +1 -1
- package/src/records/inspect.js +2 -1
- package/src/commands/profiles.js +0 -190
package/README.md
CHANGED
|
@@ -64,17 +64,18 @@ Run `jsn` for the full grouped list. Commands are organized by ServiceNow domain
|
|
|
64
64
|
|
|
65
65
|
Every command supports `--json`, `--query`, and `--help`.
|
|
66
66
|
|
|
67
|
-
## Profiles
|
|
67
|
+
## Profiles (instances)
|
|
68
68
|
|
|
69
69
|
Switch between instances without re-authenticating.
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
jsn auth login https://dev12345.service-now.com
|
|
73
|
-
jsn
|
|
74
|
-
jsn
|
|
75
|
-
jsn profiles show
|
|
73
|
+
jsn auth status
|
|
74
|
+
jsn auth switch dev12345
|
|
76
75
|
```
|
|
77
76
|
|
|
77
|
+
Add another instance with `jsn auth login <name-or-url>` — run bare for an interactive picker.
|
|
78
|
+
|
|
78
79
|
## Output Formats
|
|
79
80
|
|
|
80
81
|
| Flag | When to use |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@inquirer/core": "^10.1.0",
|
|
30
30
|
"@inquirer/prompts": "^7.5.1",
|
|
31
|
+
"better-sqlite3": "^13.0.3",
|
|
31
32
|
"chalk": "^5.4.1",
|
|
32
33
|
"cli-table3": "^0.6.5",
|
|
33
34
|
"gray-matter": "^4.0.3",
|
|
35
|
+
"turndown": "^7.2.4",
|
|
34
36
|
"yargs": "^17.7.2"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
@@ -14,7 +14,7 @@ compatibility: |
|
|
|
14
14
|
Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
|
|
15
15
|
metadata:
|
|
16
16
|
author: jacebenson
|
|
17
|
-
version: "
|
|
17
|
+
version: "4.0.0"
|
|
18
18
|
repository: https://github.com/jacebenson/jsn
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ All `list` subcommands use a paginated interactive picker in TTY mode:
|
|
|
44
44
|
2. **Use sys_id for updates** — All update/delete operations require sys_id
|
|
45
45
|
3. **Check auth first** — Run `jsn auth status` before operations
|
|
46
46
|
4. **NEVER logout** — Only run `jsn auth logout` if the user explicitly asks
|
|
47
|
-
5. **Use `--profile <name>`** to target a specific instance, or `jsn
|
|
47
|
+
5. **Use `--profile <name>`** to target a specific instance, or `jsn auth switch <name>` to change the default
|
|
48
48
|
6. **Before using `eval` or `rest`** — Ask: *"Is there a more specific jsn command?"* Prefer specific over generic over escape hatch over eval.
|
|
49
49
|
7. **CONFIRM before destructive operations** — Show what will be created/updated/deleted and ask for approval.
|
|
50
50
|
|
|
@@ -69,7 +69,7 @@ Pick the most specific tool for the job. **Never default to eval** — it's the
|
|
|
69
69
|
| **Access** | `acls`, `b4rules`, `roles`, `groups`, `users` |
|
|
70
70
|
| **UX** | `forms` (section/element layout), `lists`, `clientscripts`, `uipolicies` |
|
|
71
71
|
| **Data** | `tables`, `columns`, `includes`, `logs`, `properties`, `records` |
|
|
72
|
-
| **Config** | `setup`, `auth`, `
|
|
72
|
+
| **Config** | `setup`, `auth`, `updatesets`, `scopes` |
|
|
73
73
|
| **Docs** | `docs sync`, `docs status`, `docs search`, `docs serve` |
|
|
74
74
|
| **Developer** | `eval`, `rest`, `skill`, `version` |
|
|
75
75
|
|
package/src/app.js
CHANGED
|
@@ -120,10 +120,15 @@ export class App {
|
|
|
120
120
|
|
|
121
121
|
const scopeFormatted = `[${scope}]`;
|
|
122
122
|
|
|
123
|
+
// Read-only profiles get a lock badge on every command run, so the
|
|
124
|
+
// armed safety is always visible (not just in `auth status`).
|
|
125
|
+
const activeProfile = this.context.profileName ? this.config.profiles[this.context.profileName] : null;
|
|
126
|
+
const roBadge = activeProfile?.read_only === true ? ' 🔒' : '';
|
|
127
|
+
|
|
123
128
|
process.stderr.write('# Use `jsn scopes` to change scope, `jsn updatesets set` to change updateset\n');
|
|
124
129
|
process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
|
|
125
130
|
|
|
126
|
-
const profileStr =
|
|
131
|
+
const profileStr = `\u001b]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}${roBadge}\u001b]8;;\x07`;
|
|
127
132
|
const userStr = `]8;;${userLink}\x07${String(displayUserName).padEnd(9)}]8;;\x07`;
|
|
128
133
|
const scopeStr = `]8;;${scopeLink}\x07${String(scopeFormatted).padEnd(17)}]8;;\x07`;
|
|
129
134
|
const updateSetStr = `]8;;${updateSetLink}\x07${updateSet}]8;;\x07`;
|
|
@@ -131,9 +136,7 @@ export class App {
|
|
|
131
136
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
132
137
|
|
|
133
138
|
// ⚠️ Warning if in the Default update set (mutation commands only)
|
|
134
|
-
|
|
135
|
-
const isYolo = profile?.yolo === true;
|
|
136
|
-
if (!isYolo && isMutationCommand(argv) && updateSet && updateSet.toLowerCase().includes('default')) {
|
|
139
|
+
if (isMutationCommand(argv) && updateSet && updateSet.toLowerCase().includes('default')) {
|
|
137
140
|
const isGlobal = scope === 'global';
|
|
138
141
|
if (isGlobal) {
|
|
139
142
|
// 🔴 HARD WARNING — Global + Default
|
|
@@ -150,8 +153,6 @@ export class App {
|
|
|
150
153
|
'┃ ┃\n' +
|
|
151
154
|
'┃ Or switch to a scoped scope first: ┃\n' +
|
|
152
155
|
'┃ jsn scopes list ┃\n' +
|
|
153
|
-
'┃ ┃\n' +
|
|
154
|
-
'┃ (Run jsn updatesets yolo to silence this check) ┃\n' +
|
|
155
156
|
'┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
|
|
156
157
|
);
|
|
157
158
|
} else {
|
|
@@ -162,7 +163,6 @@ export class App {
|
|
|
162
163
|
' ⚠ Default update set in scope [' + scope + ']\n' +
|
|
163
164
|
' Changes are contained to this scope, but a named\n' +
|
|
164
165
|
' update set is still recommended for tracking.\n' +
|
|
165
|
-
' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
|
|
166
166
|
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
167
167
|
'\x1b[0m' // reset
|
|
168
168
|
);
|
package/src/auth.js
CHANGED
|
@@ -7,19 +7,34 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { password as passwordPrompt } from '@inquirer/prompts';
|
|
12
12
|
import { globalConfigDir, normalizeInstanceURL } from './config.js';
|
|
13
13
|
import { errAuth } from './errors.js';
|
|
14
14
|
|
|
15
15
|
// ─── Credential key helpers ───
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize an instance URL into a filesystem/arg-safe key part.
|
|
19
|
+
*
|
|
20
|
+
* Whitelist approach (issue #143 findings #2/#3): first map the `://`
|
|
21
|
+
* protocol separator to `_` exactly as the Go version's encoding does
|
|
22
|
+
* (so existing keyring keys like `https_dev437538.service-now.com` keep
|
|
23
|
+
* matching), then drop everything outside [a-zA-Z0-9._-]. Blacklist
|
|
24
|
+
* stripping of just `/` and `:` left quotes, `$()`, backticks, `;`,
|
|
25
|
+
* spaces, and backslashes — which broke out of double-quoted shell args
|
|
26
|
+
* in keyring calls and, on Windows, out of path.join() credential dirs.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeKeyPart(instance) {
|
|
29
|
+
return instance.replace(/:\/\//g, '_').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
/**
|
|
18
33
|
* Build a compound key for credential storage: <username>@<instance>
|
|
19
34
|
* When username is omitted (legacy), just the normalized instance URL is used.
|
|
20
35
|
*/
|
|
21
36
|
function credKey(instance, username) {
|
|
22
|
-
const normalized = instance
|
|
37
|
+
const normalized = sanitizeKeyPart(instance);
|
|
23
38
|
if (!username) return normalized;
|
|
24
39
|
return `${username.replace(/[^a-zA-Z0-9._@-]/g, '_')}@${normalized}`;
|
|
25
40
|
}
|
|
@@ -29,7 +44,7 @@ function credKey(instance, username) {
|
|
|
29
44
|
function pkceStatePath(instance) {
|
|
30
45
|
const dir = path.join(globalConfigDir(), 'pkce');
|
|
31
46
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
-
const filename = instance
|
|
47
|
+
const filename = sanitizeKeyPart(instance) + '.json';
|
|
33
48
|
return path.join(dir, filename);
|
|
34
49
|
}
|
|
35
50
|
|
|
@@ -58,24 +73,6 @@ function removePKCEState(instance) {
|
|
|
58
73
|
const DEFAULT_OAUTH_CLIENT_ID = '543e5655f77746a28228c6009a599dfb';
|
|
59
74
|
const REDIRECT_URI = '/sdk-oauth.do';
|
|
60
75
|
|
|
61
|
-
// ─── WSL detection ───
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Detect if running under Windows Subsystem for Linux.
|
|
65
|
-
* Reads /proc/version which contains "Microsoft" or "WSL" on WSL.
|
|
66
|
-
*/
|
|
67
|
-
let _cachedIsWSL = null;
|
|
68
|
-
async function isWSLEnv() {
|
|
69
|
-
if (_cachedIsWSL !== null) return _cachedIsWSL;
|
|
70
|
-
try {
|
|
71
|
-
const data = fs.readFileSync('/proc/version', 'utf-8');
|
|
72
|
-
_cachedIsWSL = /microsoft|wsl/i.test(data);
|
|
73
|
-
} catch {
|
|
74
|
-
_cachedIsWSL = false;
|
|
75
|
-
}
|
|
76
|
-
return _cachedIsWSL;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
76
|
// ─── Basic auth from environment variables ───
|
|
80
77
|
// SN_USERNAME / SN_PASSWORD — global credentials
|
|
81
78
|
// SN_<INSTANCE>_USERNAME / SN_<INSTANCE>_PASSWORD — instance-specific (e.g. SN_DEV328604_USERNAME)
|
|
@@ -151,8 +148,11 @@ function buildAuthURL(instanceURL, clientID, pkce) {
|
|
|
151
148
|
|
|
152
149
|
function keyringLookup(key) {
|
|
153
150
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
// execFileSync with an arg array — no shell, so `key` can never
|
|
152
|
+
// break out into command injection (issue #143 finding #2).
|
|
153
|
+
const result = execFileSync(
|
|
154
|
+
'secret-tool',
|
|
155
|
+
['lookup', KEYRING_ATTR_SERVICE, KEYRING_SERVICE, KEYRING_ATTR_USERNAME, key],
|
|
156
156
|
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' }
|
|
157
157
|
);
|
|
158
158
|
const trimmed = result.trim();
|
|
@@ -175,9 +175,10 @@ function keyringLookup(key) {
|
|
|
175
175
|
|
|
176
176
|
function keyringStore(key, creds) {
|
|
177
177
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
execFileSync(
|
|
179
|
+
'secret-tool',
|
|
180
|
+
['store', `--label=Password for '${key}' on '${KEYRING_SERVICE}'`,
|
|
181
|
+
KEYRING_ATTR_SERVICE, KEYRING_SERVICE, KEYRING_ATTR_USERNAME, key],
|
|
181
182
|
{ stdio: ['pipe', 'ignore', 'ignore'], input: JSON.stringify(creds) }
|
|
182
183
|
);
|
|
183
184
|
return true;
|
|
@@ -188,8 +189,9 @@ function keyringStore(key, creds) {
|
|
|
188
189
|
|
|
189
190
|
function keyringDelete(key) {
|
|
190
191
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
execFileSync(
|
|
193
|
+
'secret-tool',
|
|
194
|
+
['clear', KEYRING_ATTR_SERVICE, KEYRING_SERVICE, KEYRING_ATTR_USERNAME, key],
|
|
193
195
|
{ stdio: 'ignore' }
|
|
194
196
|
);
|
|
195
197
|
} catch {
|
|
@@ -390,43 +392,12 @@ export class AuthManager {
|
|
|
390
392
|
const authURL = buildAuthURL(instanceURL, clientID, pkce);
|
|
391
393
|
|
|
392
394
|
console.log();
|
|
393
|
-
console.log('
|
|
394
|
-
console.log('If the browser does not open automatically, visit:');
|
|
395
|
+
console.log('Open this URL in your browser to authenticate:');
|
|
395
396
|
console.log(authURL);
|
|
396
397
|
console.log();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const platform = process.platform;
|
|
401
|
-
let cmd, args;
|
|
402
|
-
if (platform === 'darwin') {
|
|
403
|
-
cmd = 'open';
|
|
404
|
-
args = [authURL];
|
|
405
|
-
} else if (platform === 'win32') {
|
|
406
|
-
cmd = 'cmd';
|
|
407
|
-
// Windows: cmd.exe interprets & as a command separator, which truncates
|
|
408
|
-
// the URL after the first parameter. Passing an empty string as the first
|
|
409
|
-
// arg to `start` serves as the window title, and spawn auto-quotes the
|
|
410
|
-
// URL arg, protecting & and other special chars from cmd.exe's parser.
|
|
411
|
-
args = ['/c', 'start', '', authURL];
|
|
412
|
-
} else {
|
|
413
|
-
// WSL detection: check if we're running under Windows Subsystem for Linux
|
|
414
|
-
// On WSL, xdg-open delegates to cmd.exe /C start <url>, which truncates
|
|
415
|
-
// on &. We call cmd.exe directly with proper quoting instead.
|
|
416
|
-
const isWSL = platform === 'linux' && (await isWSLEnv());
|
|
417
|
-
if (isWSL) {
|
|
418
|
-
cmd = 'cmd.exe';
|
|
419
|
-
args = ['/c', 'start', '', authURL];
|
|
420
|
-
} else {
|
|
421
|
-
cmd = 'xdg-open';
|
|
422
|
-
args = [authURL];
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const child = open(cmd, args, { detached: true, stdio: 'ignore' });
|
|
426
|
-
child.on('error', () => {
|
|
427
|
-
// Browser open command not available — user will open the URL manually
|
|
428
|
-
});
|
|
429
|
-
child.unref();
|
|
398
|
+
console.log('(The browser is not opened automatically — if a browser tab opens,');
|
|
399
|
+
console.log(' use the URL above if the opened page shows an error.)');
|
|
400
|
+
console.log();
|
|
430
401
|
|
|
431
402
|
console.log('After authenticating in the browser, copy the authorization code shown on the page.');
|
|
432
403
|
console.log('(input is hidden for security — just paste and press Enter)');
|
package/src/cli.js
CHANGED
|
@@ -5,15 +5,15 @@ 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, getActiveProfile, globalConfigDir } from './config.js';
|
|
9
9
|
import { App } from './app.js';
|
|
10
10
|
import { renderHelp } from './help.js';
|
|
11
11
|
import { isMutationCommand } from './mutations.js';
|
|
12
|
+
import { guardExit } from './errors.js';
|
|
12
13
|
|
|
13
14
|
// Command modules
|
|
14
15
|
import { setupCmd } from './commands/setup.js';
|
|
15
16
|
import { authCmd } from './commands/auth.js';
|
|
16
|
-
import { profilesCmd } from './commands/profiles.js';
|
|
17
17
|
import { recordsCmd } from './commands/records.js';
|
|
18
18
|
import { catalogCmd } from './commands/catalog.js';
|
|
19
19
|
import { incidentsCmd } from './commands/incidents.js';
|
|
@@ -95,14 +95,6 @@ export function buildCLI() {
|
|
|
95
95
|
const cfg = loadConfig();
|
|
96
96
|
const app = new App(cfg);
|
|
97
97
|
|
|
98
|
-
// Resolve instance once
|
|
99
|
-
let effectiveInstance;
|
|
100
|
-
try {
|
|
101
|
-
effectiveInstance = getEffectiveInstance(cfg);
|
|
102
|
-
} catch {
|
|
103
|
-
effectiveInstance = null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
98
|
return yargs(hideBin(process.argv))
|
|
107
99
|
.scriptName('jsn')
|
|
108
100
|
.usage('Usage: jsn <command> [options]')
|
|
@@ -162,8 +154,11 @@ export function buildCLI() {
|
|
|
162
154
|
if (argv.profile) {
|
|
163
155
|
const profile = (cfg.profiles || {})[argv.profile];
|
|
164
156
|
if (!profile) {
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
guardExit(argv, {
|
|
158
|
+
code: 'unknown_profile',
|
|
159
|
+
message: `Unknown profile "${argv.profile}"`,
|
|
160
|
+
hint: 'Run "jsn auth status" to list profiles.',
|
|
161
|
+
});
|
|
167
162
|
}
|
|
168
163
|
const url = profile.instance_url;
|
|
169
164
|
app._overrideInstance = url;
|
|
@@ -176,10 +171,10 @@ export function buildCLI() {
|
|
|
176
171
|
argv._overrideInstance = argv.instance;
|
|
177
172
|
}
|
|
178
173
|
},
|
|
179
|
-
// Guard mutation commands
|
|
174
|
+
// Guard mutation commands: require an instance, block read-only profiles
|
|
180
175
|
(argv) => {
|
|
181
176
|
const cmd = (argv._[0] || '').toString();
|
|
182
|
-
if (!cmd || ['help', 'version', 'completion', 'setup', 'auth', '
|
|
177
|
+
if (!cmd || ['help', 'version', 'completion', 'setup', 'auth', 'skill', 'docs'].includes(cmd)) {
|
|
183
178
|
return;
|
|
184
179
|
}
|
|
185
180
|
// Check for --instance override
|
|
@@ -187,11 +182,24 @@ export function buildCLI() {
|
|
|
187
182
|
app._overrideInstance = argv.instance;
|
|
188
183
|
argv._overrideInstance = argv.instance;
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
185
|
+
if (isMutationCommand(argv)) {
|
|
186
|
+
try {
|
|
187
|
+
app.requireInstance();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
// requireInstance throws errUsage; format it cleanly (the yargs
|
|
190
|
+
// fail handler re-throws middleware errors, which would print a
|
|
191
|
+
// raw stack trace).
|
|
192
|
+
guardExit(argv, { code: err.code || 'usage', message: err.message, hint: err.hint });
|
|
193
|
+
}
|
|
194
|
+
const profile = getActiveProfile(cfg);
|
|
195
|
+
if (profile?.read_only === true) {
|
|
196
|
+
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
197
|
+
guardExit(argv, {
|
|
198
|
+
code: 'read_only',
|
|
199
|
+
message: `Profile "${name}" is read-only. Mutations are blocked.`,
|
|
200
|
+
hint: 'Switch to a write-enabled profile first:\n jsn auth switch <name>',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
195
203
|
}
|
|
196
204
|
},
|
|
197
205
|
])
|
|
@@ -277,7 +285,6 @@ export function buildCLI() {
|
|
|
277
285
|
// CONFIGURATION
|
|
278
286
|
.command(setupCmd(wrap))
|
|
279
287
|
.command(authCmd(wrap))
|
|
280
|
-
.command(profilesCmd(wrap))
|
|
281
288
|
.command(updateSetsCmd(wrap))
|
|
282
289
|
.command(scopesCmd(wrap))
|
|
283
290
|
// CORE
|
package/src/commands/_ticket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 } from '../helpers.js';
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, assertSafeExactMatch } from '../helpers.js';
|
|
5
5
|
import { search } from '@inquirer/prompts';
|
|
6
6
|
import { isTTY, FormatAuto } from '../output.js';
|
|
7
7
|
|
|
@@ -86,6 +86,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (selectedNumber) {
|
|
89
|
+
assertSafeExactMatch(selectedNumber);
|
|
89
90
|
const showParams = new URLSearchParams();
|
|
90
91
|
showParams.set('sysparm_query', `number=${selectedNumber}`);
|
|
91
92
|
showParams.set('sysparm_display_value', 'all');
|
|
@@ -161,6 +162,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
161
162
|
describe: `Show a specific ${displayName}`,
|
|
162
163
|
handler: wrap(async (argv, app) => {
|
|
163
164
|
const number = argv.number;
|
|
165
|
+
assertSafeExactMatch(number);
|
|
164
166
|
const params = new URLSearchParams();
|
|
165
167
|
params.set('sysparm_query', `number=${number}`);
|
|
166
168
|
params.set('sysparm_display_value', 'all');
|
|
@@ -216,6 +218,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
216
218
|
.option('data', { type: 'string', demandOption: true, describe: 'JSON data to update' }),
|
|
217
219
|
handler: wrap(async (argv, app) => {
|
|
218
220
|
const number = argv.number;
|
|
221
|
+
assertSafeExactMatch(number);
|
|
219
222
|
const recordData = JSON.parse(argv.data);
|
|
220
223
|
const findParams = new URLSearchParams();
|
|
221
224
|
findParams.set('sysparm_query', `number=${number}`);
|
|
@@ -237,8 +240,11 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
237
240
|
.command({
|
|
238
241
|
command: 'delete <number>',
|
|
239
242
|
describe: `Delete a ${displayName}`,
|
|
243
|
+
builder: (y) => y.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
|
|
240
244
|
handler: wrap(async (argv, app) => {
|
|
241
245
|
const number = argv.number;
|
|
246
|
+
await confirmDelete(app, argv, `Delete ${displayName} ${number}`);
|
|
247
|
+
assertSafeExactMatch(number);
|
|
242
248
|
const findParams = new URLSearchParams();
|
|
243
249
|
findParams.set('sysparm_query', `number=${number}`);
|
|
244
250
|
findParams.set('sysparm_limit', '1');
|