@jacebenson/jsn 3.0.6 → 3.1.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.
@@ -12,114 +12,76 @@ compatibility: |
12
12
  Requires jsn CLI (github.com/jacebenson/jsn). Install via:
13
13
  npm install -g @jacebenson/jsn
14
14
  Works with Claude Code, OpenCode, Cursor, and agentskills-compatible tools.
15
-
16
- Windows users: Install Node.js first, then the npm command above. If jsn is not
17
- in your PATH, the setup command will automatically detect its location and show
18
- the correct command path.
19
15
  metadata:
20
16
  author: jacebenson
21
- version: "1.0.0"
17
+ version: "3.1.1"
22
18
  repository: https://github.com/jacebenson/jsn
23
19
  ---
24
20
 
25
- # Jace's ServiceNow CLI
21
+ # Jace's ServiceNow CLI (jsn)
26
22
 
27
- Explore and manage ServiceNow instances. Works standalone or with AI agents.
23
+ Explore and manage ServiceNow instances. Interactive paginated pickers with server-side search on all list commands.
28
24
 
29
25
  ## Discovery
30
26
 
31
- Commands are self-documenting. Use these to learn what's available:
32
-
33
27
  ```bash
34
- jsn --help # Root help with all top-level commands
35
- jsn <command> --help # Detailed usage, flags, and examples for any command
36
- jsn dev # Shows categorized dev subcommands
28
+ jsn --help # All commands grouped by domain
29
+ jsn <command> --help # Flags, subcommands, and examples
37
30
  ```
38
31
 
32
+ ## Interactive Picker
33
+
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
+
39
41
  ## Agent Rules
40
42
 
41
- 1. **Output modes** — `--json` when parsing data; `--md` when presenting to humans; `--agent` for automation
43
+ 1. **Output modes** — `--json` when parsing data; text when presenting to humans
42
44
  2. **Use sys_id for updates** — All update/delete operations require sys_id
43
45
  3. **Check auth first** — Run `jsn auth status` before operations
44
46
  4. **NEVER logout** — Only run `jsn auth logout` if the user explicitly asks
45
47
  5. **Use `--profile <name>`** to target a specific instance, or `jsn profiles use <name>` to change default
46
- 6. **Before using `eval` or `rest`** — Ask yourself: *"Have I checked if there's a more specific jsn command?"* Verify `jsn records --table <name> create` won't work first. Prefer specific over generic over escape hatch over eval.
47
- 7. **CONFIRM before destructive operations** — Always show the user exactly what will be created, updated, or deleted and ask for explicit confirmation before executing. Never run `create`, `update`, `delete`, `set`, or `remove` commands without user approval.
48
-
49
- ## ⚠️ Destructive Operations Require Confirmation
50
-
51
- **jsn does not have a `--confirm` or `--dry-run` flag.** It assumes you mean what you type. This means **YOU** (the agent) are the safety layer.
52
-
53
- **Before any write operation, you MUST:**
54
-
55
- 1. **Show the user what you found** — Run a `list` or `show` command first
56
- 2. **Present the exact command you plan to run** — Include the sys_id, table name, and data
57
- 3. **Wait for explicit approval** — "Proceed? [y/N]" or similar
58
-
59
- **Examples of destructive commands that require confirmation:**
60
- - `jsn records --table <name> create`
61
- - `jsn records --table <name> update <sys_id>`
62
- - `jsn records --table <name> delete <sys_id>`
63
- - `jsn dev updatesets set <name>`
64
- - `jsn profiles remove <name>`
65
- - Any `delete`, `update`, `create`, `set`, or `remove` subcommand
66
-
67
- **Good pattern:**
68
- ```
69
- User: "Delete that incident"
70
- Agent: "Found: INC0010001 'Server down' (sys_id: abc123...)
71
- Command: jsn records --table incident delete abc123...
72
- Proceed? [y/N]"
73
- User: "y"
74
- Agent: [runs command]
75
- ```
76
-
77
- **Bad pattern:**
78
- ```
79
- User: "Delete that incident"
80
- Agent: [immediately runs jsn records --table incident delete ...]
81
- ```
82
-
83
- ## ⚠️ AVOID `jsn eval` for Record Creation
84
-
85
- **Why:** `jsn eval` runs server-side JavaScript in the global scope. It returns HTTP 200 even when:
86
- - The insert fails due to ACL violations
87
- - Mandatory fields are missing
88
- - The record lands in the wrong scope
89
- - Logic errors prevent the insert entirely
90
-
91
- **The AI trap:** Agents see "success" and assume the record was created. It wasn't.
92
-
93
- **Always use instead:** `jsn records --table <name> create` — it returns the created record with sys_id or an explicit validation error.
94
-
95
- **Only use `eval` when:** No specific command exists, `records` lacks required fields, and `rest` doesn't work. This is rare.
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.
96
50
 
97
51
  ## Command Hierarchy
98
52
 
99
53
  Pick the most specific tool for the job. **Never default to eval** — it's the last resort:
100
54
 
101
- 1. **Specific commands** — `jsn flows`, `jsn rules`, `jsn jobs`, etc. — curated views with domain-aware formatting and validation
102
- 2. **`jsn records --table <name>`** — generic CRUD on any table (the workhorse, preferred for record creation)
103
- 3. **`jsn rest`** — raw Table API escape hatch when records command lacks required fields
104
- 4. **`jsn eval`** — ⚠️ **LAST RESORT ONLY** — server-side script execution when no other option exists
105
- 5. **Ask the human** — if none of the above work. Never generate standalone GlideRecord scripts as a fallback.
106
-
107
- ## JSON Envelope
108
-
109
- All commands support `--json`. The envelope structure:
110
-
111
- ```json
112
- {
113
- "ok": true,
114
- "data": [ ... ],
115
- "summary": "5 records",
116
- "breadcrumbs": [
117
- {"action": "show", "cmd": "jsn records --table incident <sys_id>", "description": "View details"}
118
- ]
119
- }
120
- ```
121
-
122
- Breadcrumbs suggest next commands — follow them for navigation.
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 rest`** — raw Table API escape hatch
58
+ 4. **`jsn eval`** — ⚠️ **LAST RESORT ONLY**
59
+ 5. **Ask the human** — if none of the above work
60
+
61
+ ## Key Commands
62
+
63
+ | Domain | Commands |
64
+ |--------|---------|
65
+ | **Core** | `incidents`, `changes`, `requests`, `tasks` |
66
+ | **Catalog** | `catalogitems list/show/create` — items with variables and flow info |
67
+ | **Automation** | `flows`, `actions`, `rules`, `workflows`, `triggers`, `scheduledjobs` |
68
+ | **Access** | `acls`, `b4rules`, `roles`, `groups`, `users` |
69
+ | **UX** | `forms` (section/element layout), `lists`, `clientscripts`, `uipolicies` |
70
+ | **Data** | `tables`, `columns`, `includes`, `logs`, `properties`, `records` |
71
+ | **Config** | `setup`, `auth`, `profiles`, `updatesets`, `scopes` |
72
+ | **Developer** | `eval`, `rest`, `skill`, `version` |
73
+
74
+ ## Troubleshooting Field Behavior
75
+
76
+ Check order (priority):
77
+
78
+ 1. **Before-query business rules** — `sys_script` with `when=before`, `action=query`
79
+ ```bash
80
+ jsn b4rules list --query "collection=sys_user"
81
+ ```
82
+ 2. **ACLs** — `jsn acls list --query "name=incident"`
83
+ 3. **Dictionary overrides** — `jsn records list --table sys_dictionary --query "name=incident^element=caller_id"`
84
+ 4. **Client scripts** / **UI policies** — form-level field behavior
123
85
 
124
86
  ## Configuration
125
87
 
@@ -127,11 +89,4 @@ Breadcrumbs suggest next commands — follow them for navigation.
127
89
  ~/.config/servicenow/
128
90
  ├── config.json # Profiles and settings
129
91
  └── credentials.json # Auth tokens (fallback)
130
-
131
- .servicenow/ # Per-repo config (optional)
132
- └── config.json # Project-specific settings
133
92
  ```
134
-
135
- **Environment variables:**
136
- - `SERVICENOW_TOKEN` — Override stored token
137
- - `SERVICENOW_INSTANCE` — Override instance URL
package/src/app.js CHANGED
@@ -118,7 +118,7 @@ export class App {
118
118
 
119
119
  const scopeFormatted = `[${scope}]`;
120
120
 
121
- process.stderr.write('# Use `jsn dev scopes` to change scope, `jsn dev updatesets set` to change updateset\n');
121
+ process.stderr.write('# Use `jsn scopes` to change scope, `jsn updatesets set` to change updateset\n');
122
122
  process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
123
123
 
124
124
  const profileStr = `]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}]8;;\x07`;
@@ -144,12 +144,12 @@ export class App {
144
144
  '┃ manual surgery, one change at a time. Avoid the mess. ┃\n' +
145
145
  '┃ ┃\n' +
146
146
  '┃ Create a named update set now: ┃\n' +
147
- '┃ jsn dev updatesets create --name "My Feature" ┃\n' +
147
+ '┃ jsn updatesets create --name "My Feature" ┃\n' +
148
148
  '┃ ┃\n' +
149
149
  '┃ Or switch to a scoped scope first: ┃\n' +
150
- '┃ jsn dev scopes list ┃\n' +
150
+ '┃ jsn scopes list ┃\n' +
151
151
  '┃ ┃\n' +
152
- '┃ (Run jsn dev updatesets yolo to silence this check) ┃\n' +
152
+ '┃ (Run jsn updatesets yolo to silence this check) ┃\n' +
153
153
  '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
154
154
  );
155
155
  } else {
@@ -160,7 +160,7 @@ export class App {
160
160
  ' ⚠ Default update set in scope [' + scope + ']\n' +
161
161
  ' Changes are contained to this scope, but a named\n' +
162
162
  ' update set is still recommended for tracking.\n' +
163
- ' (Run \x1b[1mjsn dev updatesets yolo\x1b[22m to silence this warning)\n' +
163
+ ' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
164
164
  '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
165
165
  '\x1b[0m' // reset
166
166
  );
package/src/cli.js CHANGED
@@ -27,6 +27,33 @@ import { versionCmd } from './commands/version.js';
27
27
  import { devCmd } from './commands/dev.js';
28
28
  import { skillCmd, checkSkill } from './commands/skill.js';
29
29
 
30
+ // Dev subcommands promoted to root level for progressive disclosure
31
+ import {
32
+ actionsCmd, includesCmd, rulesCmd,
33
+ clientScriptsCmd, uiActionsCmd, uiPoliciesCmd,
34
+ tablesCmd, columnsCmd, importCmd,
35
+ spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
36
+ aclsCmd, rolesCmd, propertiesCmd,
37
+ relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
38
+ privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
39
+ catalogscriptsCmd, cataloguipoliciesCmd,
40
+ scriptactionsCmd, scheduledjobsCmd, asyncrulesCmd, triggersCmd,
41
+ workflowsCmd, decisiontablesCmd, assignmentsCmd,
42
+ emailCmd,
43
+ restmessageCmd, restmethodsCmd, soapmessagesCmd, soapfunctionsCmd,
44
+ uimacrosCmd, uxlistsCmd, uxapplicabilityCmd,
45
+ } from './commands/dev/_simple.js';
46
+ import { flowsCmd } from './commands/dev/flows.js';
47
+ import { formsCmd } from './commands/dev/forms.js';
48
+ import { listsCmd } from './commands/dev/lists.js';
49
+ import { updateSetsCmd } from './commands/dev/updatesets.js';
50
+ import { scopesCmd } from './commands/dev/scopes.js';
51
+ import { evalCmd } from './commands/dev/eval.js';
52
+ import { restCmd } from './commands/dev/rest.js';
53
+ import { logsCmd } from './commands/dev/logs.js';
54
+ import { scrapiCmd } from './commands/dev/scrapi.js';
55
+ import { b4rulesCmd } from './commands/dev/b4rules.js';
56
+
30
57
  function wrap(handler) {
31
58
  return async (argv) => {
32
59
  try {
@@ -37,179 +64,259 @@ function wrap(handler) {
37
64
  }
38
65
  await handler(argv, app);
39
66
  } catch (err) {
40
- const app = argv.app;
41
- if (app) {
42
- app.err(err);
43
- } else {
44
- process.stderr.write(`Error: ${err.message || err}\n`);
67
+ if (err.code === 'not_found') {
68
+ const id = argv._.slice(1).join(' ') || argv.id || argv.name || argv.sysID || '';
69
+ process.stderr.write(`Error (${err.code}): ${err.message}\n`);
70
+ if (id) {
71
+ process.stderr.write(`\nHint: The identifier "${id}" was not found. Check the name or sys_id.\n`);
72
+ }
73
+ process.exit(1);
45
74
  }
75
+ if (err.code === 'usage') {
76
+ process.stderr.write(`Error (usage): ${err.message}\n`);
77
+ process.exit(2);
78
+ }
79
+ if (err.code === 'system_error') {
80
+ process.stderr.write(`Error (system): ${err.message}\n`);
81
+ process.exit(3);
82
+ }
83
+ process.stderr.write(`Error: ${err.message}\n`);
46
84
  process.exit(1);
47
85
  }
48
86
  };
49
87
  }
50
88
 
51
- export const cli = yargs(hideBin(process.argv))
52
- .scriptName('jsn')
53
- .usage('Usage: $0 <command> [options]')
54
- .option('instance', {
55
- describe: 'ServiceNow instance URL (e.g., https://dev12345.service-now.com)',
56
- type: 'string',
57
- global: true,
58
- })
59
- .option('profile', {
60
- alias: 'p',
61
- describe: 'Configuration profile to use',
62
- type: 'string',
63
- global: true,
64
- })
65
- .option('format', {
66
- describe: 'Output format: auto, json, markdown, styled, quiet',
67
- type: 'string',
68
- global: true,
69
- })
70
- .option('json', {
71
- describe: 'Output in JSON format',
72
- type: 'boolean',
73
- global: true,
74
- })
75
- .option('quiet', {
76
- alias: 'q',
77
- describe: 'Output only data, no envelope',
78
- type: 'boolean',
79
- global: true,
80
- })
81
- .option('styled', {
82
- describe: 'Force styled output',
83
- type: 'boolean',
84
- global: true,
85
- })
86
- .option('markdown', {
87
- describe: 'Output in Markdown format',
88
- type: 'boolean',
89
- global: true,
90
- })
91
- .option('no-skill-check', {
92
- describe: 'Skip the automatic skill version check',
93
- type: 'boolean',
94
- global: true,
95
- })
96
- .middleware(async (argv) => {
97
- // Determine format from flags
98
- let format = 'auto';
99
- if (argv.json) format = 'json';
100
- else if (argv.quiet) format = 'quiet';
101
- else if (argv.styled) format = 'styled';
102
- else if (argv.markdown) format = 'markdown';
103
- else if (argv.format) format = argv.format;
104
-
105
- const cfg = loadConfig({
106
- instance: argv.instance,
107
- profile: argv.profile,
108
- format,
109
- });
110
-
111
- argv.app = new App(cfg);
89
+ export function buildCLI() {
90
+ const cfg = loadConfig();
91
+ const app = new App(cfg);
112
92
 
113
- // Check auth for non-auth commands
114
- const cmd = argv._[0];
115
- const skipAuth = ['help', 'version', 'setup', 'auth', 'profiles', 'profile', 'skill', undefined].includes(cmd);
116
- if (!skipAuth) {
117
- const instance = getEffectiveInstance(cfg);
118
- if (!argv.app.auth.isAuthenticated() && instance) {
119
- process.stderr.write(`\n⚠️ Not authenticated to ${instance}\n\n`);
120
- process.stderr.write('To get started, run:\n');
121
- process.stderr.write(' jsn setup # Interactive setup\n');
122
- process.stderr.write(` jsn auth login ${instance} # Login to instance\n\n`);
123
- }
124
- }
93
+ // Resolve instance once
94
+ let effectiveInstance;
95
+ try {
96
+ effectiveInstance = getEffectiveInstance(cfg);
97
+ } catch {
98
+ effectiveInstance = null;
99
+ }
125
100
 
126
- // Read-only profile check — block mutation commands on read-only profiles
127
- const activeProfileName = cfg.activeProfile || cfg.defaultProfile;
128
- if (activeProfileName && cfg.profiles[activeProfileName] && cfg.profiles[activeProfileName].read_only) {
129
- const skipReadOnlyCheck = ['help', 'version', 'setup', 'auth', 'profiles', 'profile', 'skill', undefined].includes(cmd);
130
- if (!skipReadOnlyCheck && isMutationCommand(argv)) {
131
- process.stderr.write(`\n🔒 Profile "${activeProfileName}" is read-only.\n`);
132
- process.stderr.write(` Mutation commands are disabled on this profile.\n`);
133
- const nonReadOnlyProfiles = Object.entries(cfg.profiles || {})
134
- .filter(([n, p]) => n !== activeProfileName && !p.read_only)
135
- .map(([n]) => n);
136
- if (nonReadOnlyProfiles.length > 0) {
137
- process.stderr.write(` Switch to a writable profile: jsn profiles use ${nonReadOnlyProfiles[0]}\n`);
101
+ return yargs(hideBin(process.argv))
102
+ .scriptName('jsn')
103
+ .usage('Usage: jsn <command> [options]')
104
+ .middleware([
105
+ // Attach App instance to every command
106
+ (argv) => {
107
+ argv.app = app;
108
+ argv._profile = cfg.profiles || {};
109
+ },
110
+ ])
111
+ .options({
112
+ instance: {
113
+ type: 'string',
114
+ description: 'ServiceNow instance URL',
115
+ alias: 'i',
116
+ },
117
+ profile: {
118
+ type: 'string',
119
+ description: 'Configuration profile to use',
120
+ alias: 'p',
121
+ },
122
+ format: {
123
+ type: 'string',
124
+ description: 'Output format: auto, json, markdown, styled, quiet',
125
+ choices: ['auto', 'json', 'markdown', 'styled', 'quiet'],
126
+ },
127
+ json: {
128
+ type: 'boolean',
129
+ description: 'Output in JSON format (shortcut for --format=json)',
130
+ },
131
+ quiet: {
132
+ type: 'boolean',
133
+ description: 'Output only data, no envelope (shortcut for --format=quiet)',
134
+ alias: 'q',
135
+ },
136
+ styled: {
137
+ type: 'boolean',
138
+ description: 'Force styled output even when piped',
139
+ },
140
+ markdown: {
141
+ type: 'boolean',
142
+ description: 'Output in Markdown table format',
143
+ },
144
+ })
145
+ .middleware([
146
+ // Apply format option overrides
147
+ (argv) => {
148
+ if (argv.json) argv.format = 'json';
149
+ if (argv.quiet) argv.format = 'quiet';
150
+ if (argv.styled) argv.format = 'styled';
151
+ if (argv.markdown) argv.format = 'markdown';
152
+ if (argv.format) {
153
+ app.output.setFormatByName(argv.format);
138
154
  }
139
- process.stderr.write('\n');
140
- process.exit(1);
155
+ if (argv.instance) {
156
+ app._overrideInstance = argv.instance;
157
+ }
158
+ },
159
+ // Guard mutation commands when no instance configured
160
+ (argv) => {
161
+ const cmd = (argv._[0] || '').toString();
162
+ if (!cmd || ['help', 'version', 'completion', 'setup', 'auth', 'profiles', 'skill'].includes(cmd)) {
163
+ return;
164
+ }
165
+ // Check for --instance override
166
+ if (argv.instance) {
167
+ app._overrideInstance = argv.instance;
168
+ }
169
+ // Determine if this is a mutation
170
+ const subcommand = (argv._[1] || '').toString();
171
+ const isMutation = isMutationCommand(cmd, subcommand);
172
+ if (isMutation) {
173
+ app.requireInstance();
174
+ }
175
+ },
176
+ ])
177
+ .middleware(async (argv) => {
178
+ // Override instance for this command run
179
+ if (argv._overrideInstance) {
180
+ app.setEffectiveInstance(argv._overrideInstance);
141
181
  }
142
- }
143
182
 
144
- // Auto-check skill on every command (fire-and-forget, non-blocking)
145
- // Skipped under --json, --quiet, --no-skill-check, or env var
146
- const skipSkillCheck = ['help', 'version', 'completion', 'skill'].includes(cmd)
147
- || process.env.JSN_NO_SKILL_CHECK === '1'
148
- || argv['no-skill-check']
149
- || argv.json
150
- || argv.quiet;
151
- if (!skipSkillCheck) {
152
- // Fire check but don't await — it runs in background
153
- checkSkill().then(result => {
154
- if (result && !result.current && result.error) {
155
- process.stderr.write(`\n⚠ ${result.error}\n\n`);
156
- }
157
- }).catch(() => {});
158
- }
183
+ const cmd = (argv._[0] || '').toString();
159
184
 
160
- // Print context header for interactive terminals (at the TOP, before command output)
161
- if (!['help', 'version', 'completion', 'skill'].includes(cmd)) {
162
- await argv.app.printContextHeader(argv);
163
- }
164
- })
165
- .command(setupCmd(wrap))
166
- .command(authCmd(wrap))
167
- .command(profilesCmd(wrap))
168
- .command(recordsCmd(wrap))
169
- .command(catalogCmd(wrap))
170
- .command(incidentsCmd(wrap))
171
- .command(changesCmd(wrap))
172
- .command(requestsCmd(wrap))
173
- .command(tasksCmd(wrap))
174
- .command(usersCmd(wrap))
175
- .command(groupsCmd(wrap))
176
- .command(groupMembersCmd(wrap))
177
- .command(groupRolesCmd(wrap))
178
- .command(ticketsCmd(wrap))
179
- .command(devCmd(wrap))
180
- .command(skillCmd(wrap))
181
- .command(versionCmd(wrap))
182
- .demandCommand(1, 'You must specify a command')
183
- .help('help', 'Show help')
184
- .version(false)
185
- .strictCommands()
186
- .strictOptions(false)
187
- .fail((msg, err) => {
188
- if (err) throw err;
189
- // No command given → show custom grouped help instead of yargs error
190
- if (msg === 'You must specify a command') {
191
- // Check if --profile/-p was used without a value — show profile info instead
192
- const raw = process.argv.slice(2);
193
- const hasLoneProfile = raw.some((a, i, arr) => {
194
- if (a !== '--profile' && a !== '-p') return false;
195
- const next = arr[i + 1];
196
- return !next || next.startsWith('-');
197
- });
198
- if (hasLoneProfile) {
199
- const cfg = loadConfig();
200
- const activeName = cfg.activeProfile || cfg.defaultProfile;
201
- process.stdout.write('Profiles:\n');
202
- for (const [name, p] of Object.entries(cfg.profiles || {})) {
203
- const marker = name === activeName ? ' *' : ' ';
204
- process.stdout.write(` ${marker} ${name.padEnd(20)} ${p.instance_url || ''}\n`);
185
+ // Auto-check skill on every command (fire-and-forget, non-blocking)
186
+ const skipSkillCheck = ['help', 'version', 'completion', 'skill'].includes(cmd)
187
+ || process.env.JSN_NO_SKILL_CHECK === '1'
188
+ || argv['no-skill-check']
189
+ || argv.json
190
+ || argv.quiet;
191
+ if (!skipSkillCheck) {
192
+ checkSkill().then(result => {
193
+ if (result && !result.current && result.error) {
194
+ process.stderr.write(`\n⚠ ${result.error}\n\n`);
195
+ }
196
+ }).catch(() => {});
197
+ }
198
+
199
+ // Print context header for interactive terminals
200
+ if (!['help', 'version', 'completion', 'skill'].includes(cmd)) {
201
+ await argv.app.printContextHeader(argv);
202
+ }
203
+ })
204
+ // CONFIGURATION
205
+ .command(setupCmd(wrap))
206
+ .command(authCmd(wrap))
207
+ .command(profilesCmd(wrap))
208
+ .command(updateSetsCmd(wrap))
209
+ .command(scopesCmd(wrap))
210
+ // CORE
211
+ .command(incidentsCmd(wrap))
212
+ .command(changesCmd(wrap))
213
+ .command(requestsCmd(wrap))
214
+ .command(tasksCmd(wrap))
215
+ .command(catalogCmd(wrap))
216
+ // AUTOMATION — Async
217
+ .command(flowsCmd(wrap))
218
+ .command(actionsCmd(wrap))
219
+ .command(scriptactionsCmd(wrap))
220
+ .command(scheduledjobsCmd(wrap))
221
+ .command(asyncrulesCmd(wrap))
222
+ .command(triggersCmd(wrap))
223
+ // AUTOMATION — In Memory
224
+ .command(rulesCmd(wrap))
225
+ .command(workflowsCmd(wrap))
226
+ .command(decisiontablesCmd(wrap))
227
+ .command(assignmentsCmd(wrap))
228
+ // AUTOMATION — Inbound
229
+ .command(scrapiCmd(wrap))
230
+ .command(emailCmd(wrap))
231
+ // AUTOMATION — Outbound
232
+ .command(restmessageCmd(wrap))
233
+ .command(restmethodsCmd(wrap))
234
+ .command(soapmessagesCmd(wrap))
235
+ .command(soapfunctionsCmd(wrap))
236
+ // ACCESS
237
+ .command(aclsCmd(wrap))
238
+ .command(b4rulesCmd(wrap))
239
+ .command(rolesCmd(wrap))
240
+ .command(privilegesCmd(wrap))
241
+ .command(securitytypesCmd(wrap))
242
+ .command(aliasesCmd(wrap))
243
+ .command(groupsCmd(wrap))
244
+ .command(groupMembersCmd(wrap))
245
+ .command(groupRolesCmd(wrap))
246
+ .command(usersCmd(wrap))
247
+ // USER EXPERIENCE — Shared
248
+ .command(formsCmd(wrap))
249
+ .command(listsCmd(wrap))
250
+ .command(clientScriptsCmd(wrap))
251
+ .command(uiPoliciesCmd(wrap))
252
+ .command(uiActionsCmd(wrap))
253
+ .command(viewsCmd(wrap))
254
+ .command(catalogscriptsCmd(wrap))
255
+ .command(cataloguipoliciesCmd(wrap))
256
+ // USER EXPERIENCE — Core UI
257
+ .command(uiPagesCmd(wrap))
258
+ .command(uimacrosCmd(wrap))
259
+ .command(listcontrolsCmd(wrap))
260
+ // USER EXPERIENCE — Service Portal
261
+ .command(spPagesCmd(wrap))
262
+ .command(spWidgetsCmd(wrap))
263
+ // USER EXPERIENCE — Next Experience
264
+ .command(uxscriptsCmd(wrap))
265
+ .command(uxlistsCmd(wrap))
266
+ .command(uxapplicabilityCmd(wrap))
267
+ // USER EXPERIENCE — Navigation
268
+ .command(appMenuCmd(wrap))
269
+ .command(appmodulesCmd(wrap))
270
+ // DATA — DB Schema
271
+ .command(tablesCmd(wrap))
272
+ .command(columnsCmd(wrap))
273
+ .command(relationshipsCmd(wrap))
274
+ // DATA — Shared Code, Transforms, Logs
275
+ .command(includesCmd(wrap))
276
+ .command(importCmd(wrap))
277
+ .command(logsCmd(wrap))
278
+ .command(propertiesCmd(wrap))
279
+ // DATA
280
+ .command(recordsCmd(wrap))
281
+ .command(ticketsCmd(wrap))
282
+ // DEVELOPER
283
+ .command(evalCmd(wrap))
284
+ .command(restCmd(wrap))
285
+ .command(skillCmd(wrap))
286
+ .command(versionCmd(wrap))
287
+ // Legacy
288
+ .command(devCmd(wrap))
289
+ .demandCommand(1, 'You must specify a command')
290
+ .help('help', 'Show help')
291
+ .version(false)
292
+ .strictCommands()
293
+ .strictOptions(false)
294
+ .fail((msg, err) => {
295
+ if (err) throw err;
296
+ if (msg === 'You must specify a command') {
297
+ const raw = process.argv.slice(2);
298
+ const hasLoneProfile = raw.some((a, i, arr) => {
299
+ if (a !== '--profile' && a !== '-p') return false;
300
+ const next = arr[i + 1];
301
+ return !next || next.startsWith('-');
302
+ });
303
+ if (hasLoneProfile) {
304
+ const cfg = loadConfig();
305
+ const activeName = cfg.activeProfile || cfg.defaultProfile;
306
+ process.stdout.write('Profiles:\n');
307
+ for (const [name, p] of Object.entries(cfg.profiles || {})) {
308
+ const isActive = name === activeName;
309
+ process.stdout.write(` ${isActive ? '*' : ' '} ${name} → ${p.instance_url || '(no url)'}\n`);
310
+ }
311
+ process.exit(0);
205
312
  }
206
- process.stdout.write('\nUsage: jsn --profile <name> <command>\n');
207
- process.stdout.write(' jsn profiles use <name>\n');
313
+ process.stdout.write(renderHelp() + '\n');
208
314
  process.exit(0);
209
315
  }
210
- process.stdout.write(renderHelp());
211
- process.exit(0);
212
- }
213
- process.stderr.write(`Error: ${msg}\n`);
214
- process.exit(1);
215
- });
316
+ process.stderr.write(msg + '\n');
317
+ process.exit(1);
318
+ });
319
+ }
320
+
321
+ // Run if invoked directly (bin/jsn.js calls cli.parse())
322
+ export const cli = buildCLI();