@jacebenson/jsn 4.3.13 → 4.4.0-dev.113.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 +137 -0
- package/package.json +4 -3
- package/skills/servicenow/SKILL.md +20 -172
- package/src/app.js +29 -40
- package/src/auth-recovery.js +83 -0
- package/src/auth.js +2 -0
- package/src/capabilities.js +9 -0
- package/src/cli-adapter.js +12 -0
- package/src/cli.js +45 -9
- package/src/command-audience.js +38 -0
- package/src/commands/_generic.js +45 -28
- package/src/commands/_simple.js +30 -33
- package/src/commands/_ticket.js +24 -20
- package/src/commands/attachments.js +88 -0
- package/src/commands/auth.js +94 -19
- package/src/commands/b4rules.js +8 -3
- package/src/commands/catalog-order.js +93 -0
- package/src/commands/catalog.js +8 -10
- package/src/commands/changes.js +2 -4
- package/src/commands/cmdb-search.js +38 -0
- package/src/commands/cmdb.js +2 -0
- package/src/commands/columns.js +2 -1
- package/src/commands/customizations.js +13 -7
- package/src/commands/discover.js +54 -0
- package/src/commands/docs/db.js +9 -1
- package/src/commands/docs/docs.js +25 -2
- package/src/commands/docs/ingest.js +20 -3
- package/src/commands/docs/install.js +86 -0
- package/src/commands/docs/locks.js +34 -0
- package/src/commands/docs/refresh.js +22 -2
- package/src/commands/docs/replace.js +30 -0
- package/src/commands/docs/snapshot-check.js +11 -0
- package/src/commands/docs/snapshot.js +130 -0
- package/src/commands/docs/sync.js +16 -6
- package/src/commands/forms.js +22 -97
- package/src/commands/history.js +57 -0
- package/src/commands/incidents.js +2 -4
- package/src/commands/knowledge.js +75 -0
- package/src/commands/midservers.js +117 -10
- package/src/commands/records.js +57 -104
- package/src/commands/requests.js +9 -15
- package/src/commands/rest.js +15 -2
- package/src/commands/scan.js +234 -0
- package/src/commands/scopes.js +1 -1
- package/src/commands/setup.js +46 -9
- package/src/commands/skill.js +73 -76
- package/src/commands/tasks.js +2 -4
- package/src/commands/tickets.js +20 -19
- package/src/commands/uipolicies.js +2 -27
- package/src/commands/updatesets.js +66 -13
- package/src/commands/upgrade.js +69 -0
- package/src/commands/version.js +14 -8
- package/src/constrained-query.js +20 -0
- package/src/context.js +6 -6
- package/src/customization-guidance.js +31 -0
- package/src/customizations-report.js +6 -0
- package/src/customizations.js +28 -13
- package/src/errors.js +23 -1
- package/src/essential-records.js +40 -0
- package/src/field-behavior.js +15 -0
- package/src/help.js +12 -2
- package/src/metadata/records.js +249 -0
- package/src/midserver-linux-remote.js +210 -0
- package/src/midserver-remote-recipe.js +49 -0
- package/src/midserver-remote.js +221 -0
- package/src/midserver-windows-remote.js +318 -0
- package/src/midserver-zip.js +2 -2
- package/src/mutation-context.js +74 -0
- package/src/mutation-history.js +75 -0
- package/src/mutations.js +3 -2
- package/src/output.js +34 -2
- package/src/record-command-profiles.js +70 -0
- package/src/record-profiles.js +93 -0
- package/src/records-engine.js +57 -0
- package/src/related-lists.js +153 -0
- package/src/resolve-record.js +50 -28
- package/src/scan.js +249 -0
- package/src/sdk-batch.js +130 -0
- package/src/sdk.js +157 -18
- package/src/update-set-xml.js +89 -0
package/README.md
CHANGED
|
@@ -13,6 +13,106 @@ jsn setup
|
|
|
13
13
|
|
|
14
14
|
See the [setup guide](docs/docs-setup.md) to connect an instance safely.
|
|
15
15
|
|
|
16
|
+
### Dev channel
|
|
17
|
+
|
|
18
|
+
For the newest changes from `main`, including changes that may be unstable:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @jacebenson/jsn@dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Dev builds check the dev channel for updates during normal use, at most once
|
|
25
|
+
per day. Run `jsn version --check` to check immediately. To install the newest
|
|
26
|
+
dev build, run `jsn upgrade` (alias: `jsn update`). It preserves your installed
|
|
27
|
+
channel and skips installation when no newer version is available. Use
|
|
28
|
+
`--dry-run` to print the npm command without network access or installation,
|
|
29
|
+
`--force` to reinstall, or `--channel dev|latest` to switch channels explicitly.
|
|
30
|
+
For example, `jsn upgrade --channel latest` returns to stable. npm installation
|
|
31
|
+
permissions still apply; the command does not elevate privileges automatically.
|
|
32
|
+
|
|
33
|
+
Pushes to `main` promote the commit to `dev` and publish a tested prerelease
|
|
34
|
+
under npm's `dev` tag. Versions include the workflow run number and attempt
|
|
35
|
+
(for example, `4.3.9-dev.123.1`); they do not move npm's stable `latest` tag.
|
|
36
|
+
The npm trusted publisher for `publish-dev.yml` must authorize the repository's
|
|
37
|
+
`dev` GitHub environment, alongside the production publishing configuration.
|
|
38
|
+
|
|
39
|
+
### Knowledge search
|
|
40
|
+
|
|
41
|
+
Use `jsn knowledge search "VPN setup" --profile work` (alias `kb`) to search
|
|
42
|
+
accessible articles, then `jsn knowledge show KB0000010 --profile work` to read
|
|
43
|
+
one. Search supports bounded `--limit`/`--offset`, knowledge-base sys_ids with
|
|
44
|
+
`--kb`, and `--language`. JSON preserves native HTML; human output uses Markdown.
|
|
45
|
+
Hints preserve an explicitly selected profile/instance. The Knowledge API plugin
|
|
46
|
+
(`sn_km_api`) must be available; access is evaluated by that API, with no fallback
|
|
47
|
+
to direct table reads. Reads do not request view-count updates. See the
|
|
48
|
+
[Knowledge API reference](https://www.servicenow.com/docs/r/yokohama/api-reference/rest-apis/knowledge-api.html).
|
|
49
|
+
|
|
50
|
+
### Instance Scan
|
|
51
|
+
|
|
52
|
+
`jsn scan` manages checks and suites and runs scans against an explicit target.
|
|
53
|
+
Use `jsn scan --help` and each subcommand's `--help` to discover the options.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
jsn scan checks list --profile dev
|
|
57
|
+
jsn scan checks create --type table --data-file check.json --profile dev
|
|
58
|
+
jsn scan checks activate <check-id> --profile dev
|
|
59
|
+
jsn scan suites add-check <suite-id> --check <check-id> --profile dev
|
|
60
|
+
jsn scan run --update-set "My change" --suite "My suite" --wait --profile dev
|
|
61
|
+
jsn scan run --table sys_script_include --record <record-id> --profile dev
|
|
62
|
+
jsn scan results <execution-id> --profile dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Check types are `table`, `column`, `linter`, and `script`. Definitions are created
|
|
66
|
+
inactive unless the payload explicitly sets `active: true`; activation never
|
|
67
|
+
starts a scan. Both checks and suites support list/show/create/update/delete and
|
|
68
|
+
activate/deactivate. Suites also support add-check/remove-check.
|
|
69
|
+
|
|
70
|
+
Runs require an update set, scope, record plus table, or explicit `--full`.
|
|
71
|
+
Instance-wide scans may traverse many tables even when a check's script only
|
|
72
|
+
examines a few records. Starting and cancelling a scan require confirmation;
|
|
73
|
+
use `--force` only after approving that operation. `--wait --timeout 120` bounds
|
|
74
|
+
the wait, not the scan's lifetime. A timed-out wait keeps the execution id and
|
|
75
|
+
does not cancel or resubmit the scan. Use `status`, `results`, or `cancel` next.
|
|
76
|
+
|
|
77
|
+
Update-set scans examine current instance records, not the historical XML
|
|
78
|
+
versions. Inactive records may be skipped. Script-only checks do not participate
|
|
79
|
+
in record/update-set scans; use an explicitly instance-wide suite run to execute
|
|
80
|
+
them. Execution success does not mean zero findings or complete test coverage:
|
|
81
|
+
results report recorded check executions, finding counts, unreadable sources,
|
|
82
|
+
and truncated lists separately. A result's rerun hint preserves the previous
|
|
83
|
+
target combination and requires `--full` again for instance-wide scans.
|
|
84
|
+
|
|
85
|
+
Every command returns next-step breadcrumbs in human and JSON output, retaining
|
|
86
|
+
an explicitly selected profile or instance. `updatesets show` also offers a scan
|
|
87
|
+
hint. Recommendation/generation of new checks is not part of this command;
|
|
88
|
+
JSN contains no AI and does not infer missing business requirements.
|
|
89
|
+
|
|
90
|
+
### CMDB search
|
|
91
|
+
|
|
92
|
+
`jsn cmdb search "web" --profile work` searches names, FQDNs, IP addresses and
|
|
93
|
+
serial numbers on `cmdb_ci`. Use `--class cmdb_ci_server` for an exact class
|
|
94
|
+
match (excluding subclasses), and bounded `--limit`/`--offset` pagination.
|
|
95
|
+
Search text is literal; encoded-query operators and query scripts are rejected.
|
|
96
|
+
Existing `cmdb list --query` remains available for deliberate encoded queries.
|
|
97
|
+
Results include show/relationship hints and preserve the selected connection.
|
|
98
|
+
|
|
99
|
+
### Supported versions
|
|
100
|
+
|
|
101
|
+
Only the current release and the two preceding releases in each channel are
|
|
102
|
+
supported (three stable releases and three dev builds). For example, with
|
|
103
|
+
stable `4.3.11`, versions `4.3.11`, `4.3.10`, and `4.3.9` are supported.
|
|
104
|
+
Update to the newest version in your channel before reporting an issue.
|
|
105
|
+
|
|
106
|
+
Publishing uses npm trusted publishing (OIDC) and advances the channel's dist-tag.
|
|
107
|
+
CI/CD does not deprecate older versions or require an `NPM_TOKEN` secret. Older
|
|
108
|
+
versions remain available; their availability does not extend the support window.
|
|
109
|
+
|
|
110
|
+
The optional maintenance script `scripts/deprecate-old-versions.mjs` can preview
|
|
111
|
+
older versions with `node scripts/deprecate-old-versions.mjs latest` (or `dev`).
|
|
112
|
+
Applying deprecations with `--apply` requires npm authentication and
|
|
113
|
+
`NODE_AUTH_TOKEN`; it is separate from publishing. The script requires the
|
|
114
|
+
checkout's package version to match the channel's current npm tag.
|
|
115
|
+
|
|
16
116
|
## Try it
|
|
17
117
|
|
|
18
118
|
```bash
|
|
@@ -37,3 +137,40 @@ npm test
|
|
|
37
137
|
## License
|
|
38
138
|
|
|
39
139
|
MIT
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
### Commands for the connected account
|
|
143
|
+
|
|
144
|
+
Login and OAuth refresh check for the current account's `admin` role. Profiles
|
|
145
|
+
without that role show a compact command list focused on tickets, generic
|
|
146
|
+
records, people/groups, documentation, and connection management. If the role
|
|
147
|
+
check is unavailable, the profile also uses compact help; this does not assert
|
|
148
|
+
that the account lacks admin. Existing profiles keep full help until renewed.
|
|
149
|
+
|
|
150
|
+
This applies to root help, root JSON, `discover`, and root shell completion.
|
|
151
|
+
Use `jsn --all-commands --help` or `jsn discover --all-commands --json` to inspect
|
|
152
|
+
every registered command. Explicit command paths remain available and ServiceNow
|
|
153
|
+
ACLs decide access. Reauthenticate (or refresh OAuth) after roles change.
|
|
154
|
+
|
|
155
|
+
### Ordering catalog items
|
|
156
|
+
|
|
157
|
+
Use `jsn catalogitems search "laptop" --profile work`, then
|
|
158
|
+
`jsn catalogitems describe <item-sys-id> --profile work` to inspect pricing and
|
|
159
|
+
variable definitions through the native Service Catalog API. Create a JSON file
|
|
160
|
+
mapping variable names to values, then preview with
|
|
161
|
+
`jsn catalogitems order <item-sys-id> --variables-file variables.json --dry-run --profile work`.
|
|
162
|
+
Omit `--dry-run` to submit after confirmation. `--quantity` defaults to one;
|
|
163
|
+
`--requested-for` accepts a user sys_id subject to instance delegation rules.
|
|
164
|
+
|
|
165
|
+
Orders use `order_now`, preserving catalog fulfillment rather than inserting an
|
|
166
|
+
RITM directly. The command handles standard catalog items; record producers and
|
|
167
|
+
order guides require separate workflows. Server rules validate mandatory and
|
|
168
|
+
dynamic variables at submission; a preview does not validate those rules. Preview
|
|
169
|
+
output lists variable names, not entered values. This operation requires a
|
|
170
|
+
write-enabled profile (also for previews), but no development update set.
|
|
171
|
+
Confirmation follows the existing profile policy; automation can supply `--force`
|
|
172
|
+
only for an approved order. Timeout or incomplete response is an uncertain
|
|
173
|
+
outcome: inspect requests before retrying. JSN does not automatically submit a
|
|
174
|
+
second order. `create/list/show` retain their definition-management behavior.
|
|
175
|
+
|
|
176
|
+
See the [Service Catalog API reference](https://www.servicenow.com/docs/r/api-reference/rest-apis/c_ServiceCatalogAPI.html).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0-dev.113.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,10 +28,11 @@
|
|
|
28
28
|
"node": ">=22.5.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@inquirer/core": "^10.
|
|
32
|
-
"@inquirer/prompts": "^7.
|
|
31
|
+
"@inquirer/core": "^10.3.2",
|
|
32
|
+
"@inquirer/prompts": "^7.10.1",
|
|
33
33
|
"better-sqlite3": "^13.0.3",
|
|
34
34
|
"js-yaml": "^5.5.0",
|
|
35
|
+
"saxes": "^6.0.0",
|
|
35
36
|
"turndown": "^7.2.4"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
@@ -1,185 +1,33 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: servicenow
|
|
3
|
-
description:
|
|
4
|
-
Interact with ServiceNow instances via the jsn CLI. Use when working with ServiceNow
|
|
5
|
-
development, administration, or data exploration. Handles tables, records, business rules,
|
|
6
|
-
flows, script includes, ACLs, update sets, and more. Triggered by ServiceNow URLs
|
|
7
|
-
(service-now.com, servicenow.com) or when the user mentions ServiceNow, jsn, servicenow,
|
|
8
|
-
or related terms like tables, records, business rules, flows, script includes, ACLs,
|
|
9
|
-
update sets, or encoded queries.
|
|
3
|
+
description: Use the jsn CLI for ServiceNow development, administration, and data exploration.
|
|
10
4
|
license: MIT
|
|
11
|
-
compatibility: |
|
|
12
|
-
Requires jsn CLI (github.com/jacebenson/jsn). Install via:
|
|
13
|
-
npm install -g @jacebenson/jsn
|
|
14
|
-
Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
|
|
15
5
|
metadata:
|
|
16
6
|
author: jacebenson
|
|
17
|
-
version: "4.3.
|
|
7
|
+
version: "4.3.13"
|
|
18
8
|
repository: https://github.com/jacebenson/jsn
|
|
19
9
|
---
|
|
20
10
|
|
|
21
|
-
#
|
|
11
|
+
# ServiceNow with JSN
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
Discover commands with `jsn --help`, `jsn discover "search words"`, and
|
|
14
|
+
`jsn <command> --help`.
|
|
24
15
|
|
|
25
|
-
|
|
16
|
+
- Verify the intended instance and select it explicitly with `--profile <name>`.
|
|
17
|
+
- Prefer purpose-built or records commands over `eval`. Explain the need and get
|
|
18
|
+
approval before using eval; avoid bypassing ServiceNow protections.
|
|
19
|
+
- Honor delete confirmations. Get approval for destructive work and Global +
|
|
20
|
+
Default mutations; use `--force` only for the approved operation.
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
jsn --help # All commands grouped by domain
|
|
29
|
-
jsn <command> --help # Flags, subcommands, and examples
|
|
30
|
-
```
|
|
22
|
+
For development:
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
1. Choose or create the application scope and update set.
|
|
25
|
+
2. Do the work, keeping related changes together.
|
|
26
|
+
3. Verify the results on the instance and report where changes were captured.
|
|
27
|
+
4. Offer to run relevant existing ATFs and instance scans, or create missing checks.
|
|
28
|
+
5. Ask whether anything else belongs in the update set. If the user confirms
|
|
29
|
+
it is finished, complete it with notes.
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- Scroll past last loaded item to load next page
|
|
38
|
-
- Enter to select, Esc to cancel
|
|
39
|
-
- Falls back to table output with `--json`
|
|
40
|
-
|
|
41
|
-
## Agent Rules
|
|
42
|
-
|
|
43
|
-
1. **Output modes** — `--json` when parsing data; text when presenting to humans
|
|
44
|
-
2. **Use sys_id for updates** — All update/delete operations require sys_id
|
|
45
|
-
3. **Check auth first** — Run `jsn auth status` before operations
|
|
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 auth switch <name>` to change the default
|
|
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
|
-
7. **CONFIRM before destructive operations** — Show what will be created/updated/deleted and ask for approval.
|
|
50
|
-
|
|
51
|
-
## Command Hierarchy
|
|
52
|
-
|
|
53
|
-
Pick the most specific tool for the job. **Never default to eval** — it's the last resort:
|
|
54
|
-
|
|
55
|
-
1. **Specific commands** — `jsn flows`, `jsn rules`, `jsn catalogitems`, etc.
|
|
56
|
-
2. **`jsn records --table <name>`** — generic CRUD on any table
|
|
57
|
-
3. **`jsn docs search <query>`** — offline ServiceNow documentation search (no instance needed)
|
|
58
|
-
4. **`jsn rest`** — raw Table API escape hatch
|
|
59
|
-
5. **`jsn eval`** — ⚠️ **LAST RESORT ONLY**
|
|
60
|
-
6. **Ask the human** — if none of the above work
|
|
61
|
-
|
|
62
|
-
## Key Commands
|
|
63
|
-
|
|
64
|
-
| Domain | Commands |
|
|
65
|
-
|--------|---------|
|
|
66
|
-
| **Core** | `incidents`, `changes`, `requests`, `tasks` |
|
|
67
|
-
| **Catalog** | `catalogitems list/show/create` — items with variables and flow info |
|
|
68
|
-
| **Automation** | `flows`, `actions`, `rules`, `triggers`, `scheduledjobs` |
|
|
69
|
-
| **Access** | `acls`, `b4rules`, `roles`, `groups`, `users` |
|
|
70
|
-
| **UX** | `forms` (section/element layout), `lists`, `clientscripts`, `uipolicies` |
|
|
71
|
-
| **Data** | `tables`, `columns`, `includes`, `logs`, `properties`, `records` |
|
|
72
|
-
| **Config** | `setup`, `auth`, `updatesets`, `scopes` |
|
|
73
|
-
| **Docs** | `docs sync`, `docs status`, `docs search`, `docs serve` |
|
|
74
|
-
| **Developer** | `eval`, `rest`, `skill`, `version` |
|
|
75
|
-
|
|
76
|
-
### Customization evidence
|
|
77
|
-
|
|
78
|
-
```sh
|
|
79
|
-
jsn customizations capture --label review --customer-scope x_acme_app --json
|
|
80
|
-
jsn customizations list --json
|
|
81
|
-
jsn customizations show <run_id> --get data.categories
|
|
82
|
-
jsn customizations show <run_id> --get data.triage
|
|
83
|
-
jsn customizations html <run_id> [--out review.html]
|
|
84
|
-
jsn customizations compare <baseline_id> <new_id> --json
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Capture performs bounded GET-only table reads and saves local reports. It never
|
|
88
|
-
evals or creates, updates or deletes instance records. Standard authentication
|
|
89
|
-
may refresh OAuth tokens, store refreshed credentials locally and bootstrap a
|
|
90
|
-
session/GCK using authentication POSTs. List/show/compare are offline.
|
|
91
|
-
Declare customer ownership with repeatable `--customer-scope` values or sys_ids;
|
|
92
|
-
never infer customer ownership from a non-global scope. The declaration is
|
|
93
|
-
caller evidence, not independent publisher verification. OOTB findings reflect
|
|
94
|
-
historical upgrade skips, not a current verified baseline deviation. The upgrade
|
|
95
|
-
history is read server-side for the skip dispositions and every observed skipped
|
|
96
|
-
`file_name` is resolved against `sys_metadata` by chunked
|
|
97
|
-
`sys_update_nameIN<chunk>` lookups, so the OOTB inventory covers the observed
|
|
98
|
-
skipped artifacts; the broad `sys_metadata` read is an arbitrary `sys_id`-ordered
|
|
99
|
-
slice and is reported as a bounded sample, not an inventory, describing only the
|
|
100
|
-
unclassified and declared-customer categories. Unresolved skipped names and
|
|
101
|
-
failed lookup chunks stay explicit rather than becoming absent artifacts, and a
|
|
102
|
-
resolved artifact can lack a scope name when its scope record was outside the
|
|
103
|
-
bounded `sys_scope` read. Missing
|
|
104
|
-
ACL access, pagination limits and unmatched evidence remain explicit. All
|
|
105
|
-
reports are incomplete; no findings is not proof of no customizations.
|
|
106
|
-
Comparisons cover captured metadata/evidence, not source payloads. Removals mean
|
|
107
|
-
no longer observed, not proven deletion. Reversion impact and dependencies
|
|
108
|
-
remain unknown until separately investigated. Never recommend automatic reversion.
|
|
109
|
-
|
|
110
|
-
Upgrade history joins `file_name` to metadata `sys_update_name`. Raw disposition
|
|
111
|
-
`4` or `104` is skipped-customization evidence; resolution `reviewed_reverted`
|
|
112
|
-
excludes that history row. Use raw values, not display labels or other skip reasons.
|
|
113
|
-
|
|
114
|
-
### Complete an update set
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
jsn updatesets complete "My Feature" --notes "Fixed validation" --force --json
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
`complete [name]` defaults to the current set when the name is omitted. Explicit
|
|
121
|
-
names must be unique. Supply nonempty `--notes` in automation; real newlines
|
|
122
|
-
separate bullets. A human terminal opens a notes editor when `--notes` is absent.
|
|
123
|
-
Existing Description text is preserved and appended after confirmation. Use
|
|
124
|
-
`--force` only after approval, or the profile's `skip_confirmations` policy.
|
|
125
|
-
Notes apply only to the selected set. ServiceNow's normal batch rules can complete
|
|
126
|
-
child sets when the parent completes; their descriptions are not replaced.
|
|
127
|
-
Both selected-set Description and state are read back before success. On
|
|
128
|
-
verification failure, inspect the set before retrying.
|
|
129
|
-
|
|
130
|
-
### Table and raw REST examples
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
# Query a table with the normal records command
|
|
134
|
-
jsn records list --table incident --query "active=true" --limit 5 --json
|
|
135
|
-
|
|
136
|
-
# Get a single record
|
|
137
|
-
jsn records get --table incident --sys-id abc123... --json
|
|
138
|
-
|
|
139
|
-
# Raw endpoint access when no specific command fits
|
|
140
|
-
jsn rest "/api/now/table/incident?sysparm_limit=3" --json
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### `jsn docs` — offline documentation search
|
|
144
|
-
|
|
145
|
-
No ServiceNow instance required. Searches the full ServiceNow docs locally with FTS5 + semantic hybrid search.
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
# First run: download docs and build the index (~45k files, ~3-5 min)
|
|
149
|
-
jsn docs sync
|
|
150
|
-
|
|
151
|
-
# Check state
|
|
152
|
-
jsn docs status
|
|
153
|
-
|
|
154
|
-
# Search (keyword + semantic hybrid)
|
|
155
|
-
jsn docs search "GlideRecord query" --limit 5 --json
|
|
156
|
-
|
|
157
|
-
# Filter by bundle
|
|
158
|
-
jsn docs search "REST API" --bundle it-service-management --json
|
|
159
|
-
|
|
160
|
-
# Start the web UI
|
|
161
|
-
jsn docs serve
|
|
162
|
-
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Re-running `jsn docs sync`** does a smart incremental refresh — pulls latest docs and only rebuilds changed files (seconds, not minutes).
|
|
166
|
-
|
|
167
|
-
## Troubleshooting Field Behavior
|
|
168
|
-
|
|
169
|
-
Check order (priority):
|
|
170
|
-
|
|
171
|
-
1. **Before-query business rules** — `sys_script` with `when=before`, `action=query`
|
|
172
|
-
```bash
|
|
173
|
-
jsn b4rules list --query "collection=sys_user"
|
|
174
|
-
```
|
|
175
|
-
2. **ACLs** — `jsn acls list --query "name=incident"`
|
|
176
|
-
3. **Dictionary overrides** — `jsn records list --table sys_dictionary --query "name=incident^element=caller_id"`
|
|
177
|
-
4. **Client scripts** / **UI policies** — form-level field behavior
|
|
178
|
-
|
|
179
|
-
## Configuration
|
|
180
|
-
|
|
181
|
-
```
|
|
182
|
-
~/.config/servicenow/
|
|
183
|
-
├── config.json # Profiles and settings
|
|
184
|
-
└── credentials/ # Auth tokens (fallback, one file per key)
|
|
185
|
-
```
|
|
31
|
+
For incoming update sets: import the XML, preview the set, and resolve preview
|
|
32
|
+
errors with the human where needed. Once the preview is clear, commit the set
|
|
33
|
+
and verify the results.
|
package/src/app.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// setEffectiveInstance() are thin session-backed shims kept for the many
|
|
8
8
|
// command handlers that already call them.
|
|
9
9
|
|
|
10
|
+
import { appendMutation } from './mutation-history.js';
|
|
10
11
|
import { AuthManager } from './auth.js';
|
|
12
|
+
import { createAuthRecovery } from './auth-recovery.js';
|
|
11
13
|
import { SDKClient } from './sdk.js';
|
|
12
14
|
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled, hyperlink } from './output.js';
|
|
13
15
|
import { saveConfig } from './config.js';
|
|
@@ -15,7 +17,7 @@ import { resolveSession, applySession, contextFromSession, requireSessionInstanc
|
|
|
15
17
|
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
16
18
|
import { errUsage, errAuth } from './errors.js';
|
|
17
19
|
import { canPrompt } from './helpers.js';
|
|
18
|
-
import {
|
|
20
|
+
import { guardMutationContext } from './mutation-context.js';
|
|
19
21
|
import process from 'node:process';
|
|
20
22
|
|
|
21
23
|
|
|
@@ -53,8 +55,12 @@ export class App {
|
|
|
53
55
|
|
|
54
56
|
/** Build (or rebuild) the SDK client for an instance + session. */
|
|
55
57
|
_buildSdk(instance, session = this.session) {
|
|
56
|
-
this.sdk = new SDKClient(instance, this.auth, {
|
|
58
|
+
this.sdk = new SDKClient(instance, this.auth.createProfileProvider(instance, {
|
|
59
|
+
username: session?.username, authMethod: session?.profile?.auth_method,
|
|
60
|
+
}), {
|
|
57
61
|
domain: session?.domain || '',
|
|
62
|
+
recovery: createAuthRecovery({ app: this, session }),
|
|
63
|
+
onMutation: event => this.recordMutation(instance, session?.profileName, event),
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -63,9 +69,21 @@ export class App {
|
|
|
63
69
|
const profile = options.profile || {};
|
|
64
70
|
return new SDKClient(instance, this.auth.createProfileProvider(instance, options), {
|
|
65
71
|
domain: options.domain ?? profile.domain ?? '',
|
|
72
|
+
onMutation: event => this.recordMutation(instance, options.profileName || null, event),
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
recordMutation(instance, profile, event) {
|
|
77
|
+
if (!this._authArgv) return;
|
|
78
|
+
try { appendMutation({ ...event, instance, profile, reason: this._authArgv.reason }); }
|
|
79
|
+
catch {
|
|
80
|
+
if (!this._historyWarningShown) {
|
|
81
|
+
process.stderr.write('Warning: local mutation history could not be saved. The remote result is unchanged.\n');
|
|
82
|
+
this._historyWarningShown = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
loadContext() {
|
|
70
88
|
this.context = contextFromSession(this.session, this.config);
|
|
71
89
|
}
|
|
@@ -74,7 +92,7 @@ export class App {
|
|
|
74
92
|
return this.session?.instance || '';
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
async printContextHeader(
|
|
95
|
+
async printContextHeader(_argv = {}) {
|
|
78
96
|
if (!this.getEffectiveInstance() || !this.sdk) return;
|
|
79
97
|
if (process.env.JSN_NO_HEADER) return;
|
|
80
98
|
if (this.output.getFormat() === FormatJSON || this.output.getFormat() === FormatQuiet) return;
|
|
@@ -100,12 +118,12 @@ export class App {
|
|
|
100
118
|
|
|
101
119
|
// Both lookups depend on the user, but not on one another.
|
|
102
120
|
const [application, selectedUpdateSet] = userSysID ? await Promise.all([
|
|
103
|
-
getCurrentApplication(this.sdk, userSysID).catch(() => null),
|
|
121
|
+
getCurrentApplication(this.sdk, userSysID, { unknownWhenMissing: true }).catch(() => null),
|
|
104
122
|
getCurrentUpdateSet(this.sdk, userSysID).catch(() => null),
|
|
105
123
|
]) : [null, null];
|
|
106
|
-
const scope = application?.scope || '
|
|
124
|
+
const scope = application?.scope || 'unknown';
|
|
107
125
|
const updateSet = selectedUpdateSet?.name && selectedUpdateSet.name !== '-'
|
|
108
|
-
? selectedUpdateSet.name : '
|
|
126
|
+
? selectedUpdateSet.name : 'unknown';
|
|
109
127
|
const updateSetSysID = selectedUpdateSet?.name && selectedUpdateSet.name !== '-'
|
|
110
128
|
? selectedUpdateSet.sys_id : '';
|
|
111
129
|
this.context.scope = scope;
|
|
@@ -135,43 +153,14 @@ export class App {
|
|
|
135
153
|
|
|
136
154
|
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
137
155
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// 🔴 HARD WARNING — Global + Default
|
|
143
|
-
process.stderr.write(
|
|
144
|
-
'\x1b[31m' + // red
|
|
145
|
-
'┏━ GLOBAL + DEFAULT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n' +
|
|
146
|
-
'┃ You are in Global scope with the Default update set. ┃\n' +
|
|
147
|
-
'┃ Changes ARE captured — in the Default update set. ┃\n' +
|
|
148
|
-
'┃ Moving them to a named update set later requires ┃\n' +
|
|
149
|
-
'┃ manual surgery, one change at a time. Avoid the mess. ┃\n' +
|
|
150
|
-
'┃ ┃\n' +
|
|
151
|
-
'┃ Create a named update set now: ┃\n' +
|
|
152
|
-
'┃ jsn updatesets create --name "My Feature" ┃\n' +
|
|
153
|
-
'┃ ┃\n' +
|
|
154
|
-
'┃ Or switch to a scoped scope first: ┃\n' +
|
|
155
|
-
'┃ jsn scopes list ┃\n' +
|
|
156
|
-
'┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
|
|
157
|
-
);
|
|
158
|
-
} else {
|
|
159
|
-
// 🟡 SOFT WARNING — Scope + Default
|
|
160
|
-
process.stderr.write(
|
|
161
|
-
'\x1b[33m' + // yellow
|
|
162
|
-
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
163
|
-
' ⚠ Default update set in scope [' + scope + ']\n' +
|
|
164
|
-
' Changes are contained to this scope, but a named\n' +
|
|
165
|
-
' update set is still recommended for tracking.\n' +
|
|
166
|
-
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
167
|
-
'\x1b[0m' // reset
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async guardMutationContext(argv) {
|
|
159
|
+
this.mutationContext = await guardMutationContext(this, argv);
|
|
171
160
|
}
|
|
172
161
|
|
|
173
162
|
ok(data, opts = {}) {
|
|
174
|
-
this.output.ok(data, opts);
|
|
163
|
+
this.output.ok(data, this.mutationContext ? { ...opts, context: { ...opts.context, mutation: this.mutationContext } } : opts);
|
|
175
164
|
}
|
|
176
165
|
|
|
177
166
|
err(error) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { errAPI, errAuth } from './errors.js';
|
|
2
|
+
import { canUsePicker, quoteShellArg } from './helpers.js';
|
|
3
|
+
import { confirm } from './prompts.js';
|
|
4
|
+
|
|
5
|
+
// Recovery is scoped to the SDK's resolved identity, never the mutable default.
|
|
6
|
+
// error supplies the same safe guidance if the single retry is also rejected.
|
|
7
|
+
export function createAuthRecovery({ app, session, argv = () => app._authArgv || {},
|
|
8
|
+
canInteract = canUsePicker, prompt = confirm,
|
|
9
|
+
renewBrowser = async (request, targetApp) => {
|
|
10
|
+
const { refreshProfile } = await import('./commands/auth.js');
|
|
11
|
+
return refreshProfile(request, targetApp);
|
|
12
|
+
},
|
|
13
|
+
}) {
|
|
14
|
+
const { instance, profileName, username, profile } = session;
|
|
15
|
+
const target = profileName ? `--profile ${quoteShellArg(profileName)}` : quoteShellArg(instance);
|
|
16
|
+
const label = profileName ? `profile ${quoteShellArg(profileName)}` : 'the active profile';
|
|
17
|
+
function failure(method = profile?.auth_method) {
|
|
18
|
+
const error = errAPI(401, '');
|
|
19
|
+
const login = `jsn auth login ${target}`;
|
|
20
|
+
error.message = `Authentication rejected for ${label} (HTTP 401).`;
|
|
21
|
+
error.profile = profileName || null;
|
|
22
|
+
error.recovery = method === 'basic'
|
|
23
|
+
? `jsn auth login --basic ${quoteShellArg(instance)}${profileName ? ` --profile ${quoteShellArg(profileName)}` : ''}`
|
|
24
|
+
: method === 'gck' ? `jsn auth refresh ${target} --browser`
|
|
25
|
+
: method === 'oauth' ? `jsn auth refresh ${target}` : login;
|
|
26
|
+
error.hint = method === 'basic'
|
|
27
|
+
? `Set SN_USERNAME/SN_PASSWORD to the correct credentials, then run: ${error.recovery}`
|
|
28
|
+
: method === 'oauth'
|
|
29
|
+
? `Run interactively: ${error.recovery}. If refresh fails, run: ${login}`
|
|
30
|
+
: `Run: ${error.recovery}`;
|
|
31
|
+
return error;
|
|
32
|
+
}
|
|
33
|
+
// Keep the settled promise too: this provider gets only one recovery attempt.
|
|
34
|
+
let recoveryAttempt;
|
|
35
|
+
const provider = {
|
|
36
|
+
error: failure(),
|
|
37
|
+
credentialError(cause) {
|
|
38
|
+
// Local validation can fail before any HTTP response. Keep its precise
|
|
39
|
+
// reason, but target recovery to this invocation rather than the default.
|
|
40
|
+
const error = errAuth(`${cause.message} (${label}).`);
|
|
41
|
+
error.profile = profileName || null;
|
|
42
|
+
error.recovery = `jsn auth login ${target}`;
|
|
43
|
+
error.hint = `Run: ${error.recovery}`;
|
|
44
|
+
return error;
|
|
45
|
+
},
|
|
46
|
+
async recover({ status }) {
|
|
47
|
+
if (status !== 401) return false;
|
|
48
|
+
return recoveryAttempt ??= recoverOnce();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
async function recoverOnce() {
|
|
52
|
+
let creds;
|
|
53
|
+
try {
|
|
54
|
+
creds = await app.auth.getCredentialsFor(instance, username, {
|
|
55
|
+
username, authMethod: profile?.auth_method, refresh: false,
|
|
56
|
+
});
|
|
57
|
+
} catch { throw provider.error; }
|
|
58
|
+
const method = profile?.auth_method || creds.auth_method;
|
|
59
|
+
provider.error = failure(method);
|
|
60
|
+
if (method === 'oauth' && creds.refresh_token) {
|
|
61
|
+
try {
|
|
62
|
+
await app.auth.refreshToken(instance, creds, username ?? null);
|
|
63
|
+
return true;
|
|
64
|
+
} catch { throw provider.error; }
|
|
65
|
+
}
|
|
66
|
+
if (method === 'gck' && profileName && ['auto', 'confirm'].includes(profile?.reauth ?? 'confirm')
|
|
67
|
+
&& canInteract(app, argv())) {
|
|
68
|
+
try {
|
|
69
|
+
if ((profile?.reauth ?? 'confirm') === 'confirm'
|
|
70
|
+
&& !await prompt({ message: `Sign in again in a browser for ${label}?`, default: true })) throw provider.error;
|
|
71
|
+
// Reuse the canonical GCK refresh/login operation without emitting a
|
|
72
|
+
// second success envelope or replacing the in-flight command's SDK.
|
|
73
|
+
const targetApp = Object.create(app);
|
|
74
|
+
targetApp.ok = () => {};
|
|
75
|
+
targetApp.sdk = app.getSDKForProfile(instance, { profile, authMethod: 'gck', username });
|
|
76
|
+
await renewBrowser({ profile: profileName, browser: true }, targetApp);
|
|
77
|
+
return true;
|
|
78
|
+
} catch { throw provider.error; }
|
|
79
|
+
}
|
|
80
|
+
throw provider.error;
|
|
81
|
+
}
|
|
82
|
+
return provider;
|
|
83
|
+
}
|
package/src/auth.js
CHANGED
|
@@ -612,7 +612,9 @@ export class AuthManager {
|
|
|
612
612
|
if (!instance) {
|
|
613
613
|
throw errAuth('No instance specified');
|
|
614
614
|
}
|
|
615
|
+
const existed = !!this.credentialStore.load(instance, username);
|
|
615
616
|
this.credentialStore.delete(instance, username);
|
|
617
|
+
return existed && !this.credentialStore.load(instance, username);
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
/**
|
package/src/capabilities.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
// skipDailyChecks: true,
|
|
17
17
|
// // Suppress the current instance header for profile management commands.
|
|
18
18
|
// skipContextHeader: true,
|
|
19
|
+
// // Remediation commands bypass mutation capture-context approval only.
|
|
20
|
+
// mutationContextExemptSubcommands: ['create', 'set'],
|
|
21
|
+
// // REST methods that are reads (read-only profile policy is separate).
|
|
22
|
+
// mutationContextReadMethods: ['GET', 'HEAD', 'OPTIONS'],
|
|
19
23
|
// }
|
|
20
24
|
//
|
|
21
25
|
// cli.js derives every skip-list and the mutation guard's path list from
|
|
@@ -130,3 +134,8 @@ export function mutationPaths(caps = collectCapabilities()) {
|
|
|
130
134
|
export function _resetCapabilities() {
|
|
131
135
|
REGISTRY.clear();
|
|
132
136
|
}
|
|
137
|
+
|
|
138
|
+
/** Declared local-only previews bypass instance access, including context headers. */
|
|
139
|
+
export function isLocalPreview(argv, caps = collectCapabilities()) {
|
|
140
|
+
return argv.dryRun === true && caps.get(argv._?.[0])?.localPreviewSubcommands?.includes(argv._?.[1]) === true;
|
|
141
|
+
}
|
package/src/cli-adapter.js
CHANGED
|
@@ -116,6 +116,7 @@ export class LocalCLI {
|
|
|
116
116
|
|
|
117
117
|
scriptName(value) { this.script = value; return this; }
|
|
118
118
|
usage(value) { this.root.usageText = value; return this; }
|
|
119
|
+
epilogue(value) { this.root.epilogueText = value; return this; }
|
|
119
120
|
middleware(fn) { this.middlewares.push(...(Array.isArray(fn) ? fn : [fn])); return this; }
|
|
120
121
|
option(name, config) { this.root.options.set(name, { ...config, name }); return this; }
|
|
121
122
|
options(config) { for (const [name, value] of Object.entries(config)) this.option(name, value); return this; }
|
|
@@ -294,6 +295,13 @@ export class LocalCLI {
|
|
|
294
295
|
aliases: [...node.aliases],
|
|
295
296
|
description: node.definition.describe || '',
|
|
296
297
|
subcommands: node.children.filter(visible).map((child) => child.name),
|
|
298
|
+
discovery: node.definition.discovery || null,
|
|
299
|
+
help: `jsn ${node.name} --help`,
|
|
300
|
+
actions: node.children.filter(visible).map((child) => ({
|
|
301
|
+
name: child.name,
|
|
302
|
+
description: child.definition.describe || '',
|
|
303
|
+
help: `jsn ${node.name} ${child.name} --help`,
|
|
304
|
+
})),
|
|
297
305
|
};
|
|
298
306
|
};
|
|
299
307
|
return {
|
|
@@ -324,6 +332,10 @@ export class LocalCLI {
|
|
|
324
332
|
}
|
|
325
333
|
|
|
326
334
|
_help(node, path = []) {
|
|
335
|
+
if (!path.length && this.rootHelpRenderer) {
|
|
336
|
+
process.stdout.write(this.rootHelpRenderer() + "\n");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
327
339
|
const lines = [node.usageText || `Usage: ${this.script} ${path.join(' ') || '<command>'}`];
|
|
328
340
|
if (node.definition.describe) lines.push(`\n${node.definition.describe}`);
|
|
329
341
|
if (node.children.length) lines.push('\nCommands:\n' + node.children.filter((n) => n.name !== this.completionCommand).map((n) => ` ${n.definition.command} ${n.definition.describe || ''}`).join('\n'));
|