@jacebenson/jsn 4.3.12 → 4.3.13-dev.101.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 +95 -0
- package/package.json +2 -1
- 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 +24 -4
- package/src/cli.js +41 -9
- package/src/command-audience.js +35 -0
- package/src/commands/_generic.js +58 -37
- package/src/commands/_simple.js +30 -33
- package/src/commands/_ticket.js +38 -31
- package/src/commands/attachments.js +88 -0
- package/src/commands/auth.js +94 -19
- package/src/commands/b4rules.js +8 -3
- package/src/commands/changes.js +2 -4
- 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/groups.js +11 -8
- package/src/commands/history.js +57 -0
- package/src/commands/incidents.js +2 -4
- package/src/commands/midservers.js +117 -10
- package/src/commands/records.js +64 -119
- package/src/commands/requests.js +10 -16
- 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 +72 -76
- package/src/commands/tasks.js +2 -4
- package/src/commands/tickets.js +31 -22
- package/src/commands/uipolicies.js +2 -27
- package/src/commands/updatesets.js +66 -13
- package/src/commands/users.js +11 -8
- 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 +11 -2
- package/src/helpers.js +44 -0
- 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,87 @@ 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 `npm install -g @jacebenson/jsn@dev` again. Updates are manual.
|
|
27
|
+
Return to stable with `npm install -g @jacebenson/jsn@latest`.
|
|
28
|
+
|
|
29
|
+
Pushes to `main` promote the commit to `dev` and publish a tested prerelease
|
|
30
|
+
under npm's `dev` tag. Versions include the workflow run number and attempt
|
|
31
|
+
(for example, `4.3.9-dev.123.1`); they do not move npm's stable `latest` tag.
|
|
32
|
+
The npm trusted publisher for `publish-dev.yml` must authorize the repository's
|
|
33
|
+
`dev` GitHub environment, alongside the production publishing configuration.
|
|
34
|
+
|
|
35
|
+
### Instance Scan
|
|
36
|
+
|
|
37
|
+
`jsn scan` manages checks and suites and runs scans against an explicit target.
|
|
38
|
+
Use `jsn scan --help` and each subcommand's `--help` to discover the options.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
jsn scan checks list --profile dev
|
|
42
|
+
jsn scan checks create --type table --data-file check.json --profile dev
|
|
43
|
+
jsn scan checks activate <check-id> --profile dev
|
|
44
|
+
jsn scan suites add-check <suite-id> --check <check-id> --profile dev
|
|
45
|
+
jsn scan run --update-set "My change" --suite "My suite" --wait --profile dev
|
|
46
|
+
jsn scan run --table sys_script_include --record <record-id> --profile dev
|
|
47
|
+
jsn scan results <execution-id> --profile dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Check types are `table`, `column`, `linter`, and `script`. Definitions are created
|
|
51
|
+
inactive unless the payload explicitly sets `active: true`; activation never
|
|
52
|
+
starts a scan. Both checks and suites support list/show/create/update/delete and
|
|
53
|
+
activate/deactivate. Suites also support add-check/remove-check.
|
|
54
|
+
|
|
55
|
+
Runs require an update set, scope, record plus table, or explicit `--full`.
|
|
56
|
+
Instance-wide scans may traverse many tables even when a check's script only
|
|
57
|
+
examines a few records. Starting and cancelling a scan require confirmation;
|
|
58
|
+
use `--force` only after approving that operation. `--wait --timeout 120` bounds
|
|
59
|
+
the wait, not the scan's lifetime. A timed-out wait keeps the execution id and
|
|
60
|
+
does not cancel or resubmit the scan. Use `status`, `results`, or `cancel` next.
|
|
61
|
+
|
|
62
|
+
Update-set scans examine current instance records, not the historical XML
|
|
63
|
+
versions. Inactive records may be skipped. Script-only checks do not participate
|
|
64
|
+
in record/update-set scans; use an explicitly instance-wide suite run to execute
|
|
65
|
+
them. Execution success does not mean zero findings or complete test coverage:
|
|
66
|
+
results report recorded check executions, finding counts, unreadable sources,
|
|
67
|
+
and truncated lists separately. A result's rerun hint preserves the previous
|
|
68
|
+
target combination and requires `--full` again for instance-wide scans.
|
|
69
|
+
|
|
70
|
+
Every command returns next-step breadcrumbs in human and JSON output, retaining
|
|
71
|
+
an explicitly selected profile or instance. `updatesets show` also offers a scan
|
|
72
|
+
hint. Recommendation/generation of new checks is not part of this command;
|
|
73
|
+
JSN contains no AI and does not infer missing business requirements.
|
|
74
|
+
|
|
75
|
+
### Supported versions
|
|
76
|
+
|
|
77
|
+
Only the current release and the two preceding releases in each channel are
|
|
78
|
+
supported (three stable releases and three dev builds). For example, with
|
|
79
|
+
stable `4.3.11`, versions `4.3.11`, `4.3.10`, and `4.3.9` are supported.
|
|
80
|
+
Update to the newest version in your channel before reporting an issue.
|
|
81
|
+
|
|
82
|
+
After each successful npm publish, deployment deprecates older versions in that
|
|
83
|
+
channel. Deprecated versions remain installable and appear in history, but npm
|
|
84
|
+
prints an update warning on installation. Existing deprecation messages are
|
|
85
|
+
preserved. No versions are unpublished.
|
|
86
|
+
|
|
87
|
+
The deprecation step uses the repository's `NPM_TOKEN` secret with permission to
|
|
88
|
+
modify `@jacebenson/jsn` and noninteractive authentication (including bypass 2FA
|
|
89
|
+
where required). Publishing continues to use OIDC. A deprecation failure fails
|
|
90
|
+
the workflow after publishing; it does not roll back the published package.
|
|
91
|
+
To retry only deprecation after fixing authentication, use the matching release
|
|
92
|
+
checkout and run `node scripts/deprecate-old-versions.mjs latest --apply` (or
|
|
93
|
+
`dev --apply`) with npm authentication configured and `NODE_AUTH_TOKEN` set.
|
|
94
|
+
Omit `--apply` to preview the affected versions without changing npm. The script
|
|
95
|
+
requires the checkout's package version to match the channel's current npm tag.
|
|
96
|
+
|
|
16
97
|
## Try it
|
|
17
98
|
|
|
18
99
|
```bash
|
|
@@ -37,3 +118,17 @@ npm test
|
|
|
37
118
|
## License
|
|
38
119
|
|
|
39
120
|
MIT
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
### Commands for the connected account
|
|
124
|
+
|
|
125
|
+
Login and OAuth refresh check for the current account's `admin` role. Profiles
|
|
126
|
+
without that role show a compact command list focused on tickets, generic
|
|
127
|
+
records, people/groups, documentation, and connection management. If the role
|
|
128
|
+
check is unavailable, the profile also uses compact help; this does not assert
|
|
129
|
+
that the account lacks admin. Existing profiles keep full help until renewed.
|
|
130
|
+
|
|
131
|
+
This applies to root help, root JSON, `discover`, and root shell completion.
|
|
132
|
+
Use `jsn --all-commands --help` or `jsn discover --all-commands --json` to inspect
|
|
133
|
+
every registered command. Explicit command paths remain available and ServiceNow
|
|
134
|
+
ACLs decide access. Reauthenticate (or refresh OAuth) after roles change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.13-dev.101.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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"@inquirer/prompts": "^7.5.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: false })) 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
|
@@ -3,6 +3,14 @@ import process from 'node:process';
|
|
|
3
3
|
|
|
4
4
|
const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
5
5
|
const names = (x) => typeof x === 'string' ? [x] : (x || []);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A repeatable option may be declared either way: `type: 'array'` (this
|
|
9
|
+
* adapter's historic spelling) or `array: true` (the yargs spelling most
|
|
10
|
+
* contributors reach for). Both must accumulate; honoring only the first
|
|
11
|
+
* silently kept the LAST value of `-f a=1 -f b=2` and dropped the rest.
|
|
12
|
+
*/
|
|
13
|
+
const isArrayOption = (spec) => spec?.type === 'array' || spec?.array === true;
|
|
6
14
|
const commandName = (command) => command.trim().split(/\s+/)[0];
|
|
7
15
|
const positionSpec = (command) => [...command.matchAll(/([<[])([^>\]]+)[>\]]/g)].map((m) => ({ name: m[2], required: m[1] === '<' }));
|
|
8
16
|
|
|
@@ -108,6 +116,7 @@ export class LocalCLI {
|
|
|
108
116
|
|
|
109
117
|
scriptName(value) { this.script = value; return this; }
|
|
110
118
|
usage(value) { this.root.usageText = value; return this; }
|
|
119
|
+
epilogue(value) { this.root.epilogueText = value; return this; }
|
|
111
120
|
middleware(fn) { this.middlewares.push(...(Array.isArray(fn) ? fn : [fn])); return this; }
|
|
112
121
|
option(name, config) { this.root.options.set(name, { ...config, name }); return this; }
|
|
113
122
|
options(config) { for (const [name, value] of Object.entries(config)) this.option(name, value); return this; }
|
|
@@ -189,7 +198,7 @@ export class LocalCLI {
|
|
|
189
198
|
_assign(argv, key, value, spec, opts = {}) {
|
|
190
199
|
const validate = opts.validate !== false;
|
|
191
200
|
const actual = spec?.name || key;
|
|
192
|
-
const values = spec
|
|
201
|
+
const values = isArrayOption(spec) ? (Array.isArray(value) ? value : [value]) : [value];
|
|
193
202
|
if (validate && spec?.choices && values.some((item) => !spec.choices.includes(item))) {
|
|
194
203
|
this._fail(`Invalid value for --${actual}: ${value}. Choose from: ${spec.choices.join(', ')}`);
|
|
195
204
|
return false;
|
|
@@ -201,7 +210,7 @@ export class LocalCLI {
|
|
|
201
210
|
const converted = spec?.type === 'boolean'
|
|
202
211
|
? (value === 'true' ? true : value === 'false' ? false : value)
|
|
203
212
|
: spec?.type === 'number' ? Number(value) : value;
|
|
204
|
-
if (spec
|
|
213
|
+
if (isArrayOption(spec)) argv[actual] = [...(argv[actual] || []), ...values];
|
|
205
214
|
else argv[actual] = converted;
|
|
206
215
|
argv[camel(actual)] = argv[actual];
|
|
207
216
|
for (const alias of names(spec?.alias)) argv[alias] = argv[actual];
|
|
@@ -210,8 +219,8 @@ export class LocalCLI {
|
|
|
210
219
|
const options = this._optionConfig(path);
|
|
211
220
|
const defs = {};
|
|
212
221
|
for (const [key, spec] of options) defs[key] = {
|
|
213
|
-
type: spec.type === 'number' || spec
|
|
214
|
-
multiple: spec
|
|
222
|
+
type: spec.type === 'number' || isArrayOption(spec) ? 'string' : (spec.type || 'boolean'),
|
|
223
|
+
multiple: isArrayOption(spec),
|
|
215
224
|
};
|
|
216
225
|
args = args.map((arg) => {
|
|
217
226
|
if (/^-[^-]=/.test(arg)) return `--${arg.slice(1)}`;
|
|
@@ -286,6 +295,13 @@ export class LocalCLI {
|
|
|
286
295
|
aliases: [...node.aliases],
|
|
287
296
|
description: node.definition.describe || '',
|
|
288
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
|
+
})),
|
|
289
305
|
};
|
|
290
306
|
};
|
|
291
307
|
return {
|
|
@@ -316,6 +332,10 @@ export class LocalCLI {
|
|
|
316
332
|
}
|
|
317
333
|
|
|
318
334
|
_help(node, path = []) {
|
|
335
|
+
if (!path.length && this.rootHelpRenderer) {
|
|
336
|
+
process.stdout.write(this.rootHelpRenderer() + "\n");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
319
339
|
const lines = [node.usageText || `Usage: ${this.script} ${path.join(' ') || '<command>'}`];
|
|
320
340
|
if (node.definition.describe) lines.push(`\n${node.definition.describe}`);
|
|
321
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'));
|