@jacebenson/jsn 4.3.13 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +137 -0
  2. package/package.json +4 -3
  3. package/skills/servicenow/SKILL.md +20 -172
  4. package/src/app.js +29 -40
  5. package/src/auth-recovery.js +83 -0
  6. package/src/auth.js +2 -0
  7. package/src/capabilities.js +9 -0
  8. package/src/cli-adapter.js +12 -0
  9. package/src/cli.js +45 -9
  10. package/src/command-audience.js +38 -0
  11. package/src/commands/_generic.js +45 -28
  12. package/src/commands/_simple.js +30 -33
  13. package/src/commands/_ticket.js +24 -20
  14. package/src/commands/attachments.js +88 -0
  15. package/src/commands/auth.js +94 -19
  16. package/src/commands/b4rules.js +8 -3
  17. package/src/commands/catalog-order.js +93 -0
  18. package/src/commands/catalog.js +8 -10
  19. package/src/commands/changes.js +2 -4
  20. package/src/commands/cmdb-search.js +38 -0
  21. package/src/commands/cmdb.js +2 -0
  22. package/src/commands/columns.js +2 -1
  23. package/src/commands/customizations.js +13 -7
  24. package/src/commands/discover.js +54 -0
  25. package/src/commands/docs/db.js +9 -1
  26. package/src/commands/docs/docs.js +25 -2
  27. package/src/commands/docs/ingest.js +20 -3
  28. package/src/commands/docs/install.js +86 -0
  29. package/src/commands/docs/locks.js +34 -0
  30. package/src/commands/docs/refresh.js +22 -2
  31. package/src/commands/docs/replace.js +30 -0
  32. package/src/commands/docs/snapshot-check.js +11 -0
  33. package/src/commands/docs/snapshot.js +130 -0
  34. package/src/commands/docs/sync.js +16 -6
  35. package/src/commands/forms.js +22 -97
  36. package/src/commands/history.js +57 -0
  37. package/src/commands/incidents.js +2 -4
  38. package/src/commands/knowledge.js +75 -0
  39. package/src/commands/midservers.js +117 -10
  40. package/src/commands/records.js +57 -104
  41. package/src/commands/requests.js +9 -15
  42. package/src/commands/rest.js +15 -2
  43. package/src/commands/scan.js +234 -0
  44. package/src/commands/scopes.js +1 -1
  45. package/src/commands/setup.js +46 -9
  46. package/src/commands/skill.js +73 -76
  47. package/src/commands/tasks.js +2 -4
  48. package/src/commands/tickets.js +20 -19
  49. package/src/commands/uipolicies.js +2 -27
  50. package/src/commands/updatesets.js +66 -13
  51. package/src/commands/upgrade.js +69 -0
  52. package/src/commands/version.js +14 -8
  53. package/src/constrained-query.js +20 -0
  54. package/src/context.js +6 -6
  55. package/src/customization-guidance.js +31 -0
  56. package/src/customizations-report.js +6 -0
  57. package/src/customizations.js +28 -13
  58. package/src/errors.js +23 -1
  59. package/src/essential-records.js +40 -0
  60. package/src/field-behavior.js +15 -0
  61. package/src/help.js +12 -2
  62. package/src/metadata/records.js +249 -0
  63. package/src/midserver-linux-remote.js +210 -0
  64. package/src/midserver-remote-recipe.js +49 -0
  65. package/src/midserver-remote.js +221 -0
  66. package/src/midserver-windows-remote.js +318 -0
  67. package/src/midserver-zip.js +2 -2
  68. package/src/mutation-context.js +74 -0
  69. package/src/mutation-history.js +75 -0
  70. package/src/mutations.js +3 -2
  71. package/src/output.js +34 -2
  72. package/src/record-command-profiles.js +70 -0
  73. package/src/record-profiles.js +93 -0
  74. package/src/records-engine.js +57 -0
  75. package/src/related-lists.js +153 -0
  76. package/src/resolve-record.js +50 -28
  77. package/src/scan.js +249 -0
  78. package/src/sdk-batch.js +130 -0
  79. package/src/sdk.js +157 -18
  80. 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.13",
3
+ "version": "4.4.0",
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.1.0",
32
- "@inquirer/prompts": "^7.5.1",
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.6"
7
+ version: "4.3.13"
18
8
  repository: https://github.com/jacebenson/jsn
19
9
  ---
20
10
 
21
- # Jace's ServiceNow CLI (jsn)
11
+ # ServiceNow with JSN
22
12
 
23
- Explore and manage ServiceNow instances. Interactive paginated pickers with server-side search on all list commands.
13
+ Discover commands with `jsn --help`, `jsn discover "search words"`, and
14
+ `jsn <command> --help`.
24
15
 
25
- ## Discovery
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
- ```bash
28
- jsn --help # All commands grouped by domain
29
- jsn <command> --help # Flags, subcommands, and examples
30
- ```
22
+ For development:
31
23
 
32
- ## Interactive Picker
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
- All `list` subcommands use a paginated interactive picker in TTY mode:
35
- - Arrow through items (10 per page)
36
- - Type to search (server-side LIKE query)
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 { isMutationCommand } from './mutations.js';
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(argv = {}) {
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 || 'global';
124
+ const scope = application?.scope || 'unknown';
107
125
  const updateSet = selectedUpdateSet?.name && selectedUpdateSet.name !== '-'
108
- ? selectedUpdateSet.name : 'Default';
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
- // ⚠️ Warning if in the Default update set (mutation commands only)
139
- if (isMutationCommand(argv) && updateSet && updateSet.toLowerCase().includes('default')) {
140
- const isGlobal = scope === 'global';
141
- if (isGlobal) {
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
  /**
@@ -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
+ }
@@ -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'));