@jacebenson/jsn 3.1.0 → 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.
package/src/cli.js CHANGED
@@ -37,6 +37,11 @@ import {
37
37
  relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
38
38
  privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
39
39
  catalogscriptsCmd, cataloguipoliciesCmd,
40
+ scriptactionsCmd, scheduledjobsCmd, asyncrulesCmd, triggersCmd,
41
+ workflowsCmd, decisiontablesCmd, assignmentsCmd,
42
+ emailCmd,
43
+ restmessageCmd, restmethodsCmd, soapmessagesCmd, soapfunctionsCmd,
44
+ uimacrosCmd, uxlistsCmd, uxapplicabilityCmd,
40
45
  } from './commands/dev/_simple.js';
41
46
  import { flowsCmd } from './commands/dev/flows.js';
42
47
  import { formsCmd } from './commands/dev/forms.js';
@@ -47,6 +52,7 @@ import { evalCmd } from './commands/dev/eval.js';
47
52
  import { restCmd } from './commands/dev/rest.js';
48
53
  import { logsCmd } from './commands/dev/logs.js';
49
54
  import { scrapiCmd } from './commands/dev/scrapi.js';
55
+ import { b4rulesCmd } from './commands/dev/b4rules.js';
50
56
 
51
57
  function wrap(handler) {
52
58
  return async (argv) => {
@@ -58,218 +64,259 @@ function wrap(handler) {
58
64
  }
59
65
  await handler(argv, app);
60
66
  } catch (err) {
61
- const app = argv.app;
62
- if (app) {
63
- app.err(err);
64
- } else {
65
- 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);
74
+ }
75
+ if (err.code === 'usage') {
76
+ process.stderr.write(`Error (usage): ${err.message}\n`);
77
+ process.exit(2);
66
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`);
67
84
  process.exit(1);
68
85
  }
69
86
  };
70
87
  }
71
88
 
72
- export const cli = yargs(hideBin(process.argv))
73
- .scriptName('jsn')
74
- .usage('Usage: $0 <command> [options]')
75
- .option('instance', {
76
- describe: 'ServiceNow instance URL (e.g., https://dev12345.service-now.com)',
77
- type: 'string',
78
- global: true,
79
- })
80
- .option('profile', {
81
- alias: 'p',
82
- describe: 'Configuration profile to use',
83
- type: 'string',
84
- global: true,
85
- })
86
- .option('format', {
87
- describe: 'Output format: auto, json, markdown, styled, quiet',
88
- type: 'string',
89
- global: true,
90
- })
91
- .option('json', {
92
- describe: 'Output in JSON format',
93
- type: 'boolean',
94
- global: true,
95
- })
96
- .option('quiet', {
97
- alias: 'q',
98
- describe: 'Output only data, no envelope',
99
- type: 'boolean',
100
- global: true,
101
- })
102
- .option('styled', {
103
- describe: 'Force styled output',
104
- type: 'boolean',
105
- global: true,
106
- })
107
- .option('markdown', {
108
- describe: 'Output in Markdown format',
109
- type: 'boolean',
110
- global: true,
111
- })
112
- .option('no-skill-check', {
113
- describe: 'Skip the automatic skill version check',
114
- type: 'boolean',
115
- global: true,
116
- })
117
- .middleware(async (argv) => {
118
- // Determine format from flags
119
- let format = 'auto';
120
- if (argv.json) format = 'json';
121
- else if (argv.quiet) format = 'quiet';
122
- else if (argv.styled) format = 'styled';
123
- else if (argv.markdown) format = 'markdown';
124
- else if (argv.format) format = argv.format;
125
-
126
- const cfg = loadConfig({
127
- instance: argv.instance,
128
- profile: argv.profile,
129
- format,
130
- });
131
-
132
- argv.app = new App(cfg);
89
+ export function buildCLI() {
90
+ const cfg = loadConfig();
91
+ const app = new App(cfg);
133
92
 
134
- // Check auth for non-auth commands
135
- const cmd = argv._[0];
136
- const skipAuth = ['help', 'version', 'setup', 'auth', 'profiles', 'profile', 'skill', undefined].includes(cmd);
137
- if (!skipAuth) {
138
- const instance = getEffectiveInstance(cfg);
139
- if (!argv.app.auth.isAuthenticated() && instance) {
140
- process.stderr.write(`\n⚠️ Not authenticated to ${instance}\n\n`);
141
- process.stderr.write('To get started, run:\n');
142
- process.stderr.write(' jsn setup # Interactive setup\n');
143
- process.stderr.write(` jsn auth login ${instance} # Login to instance\n\n`);
144
- }
145
- }
93
+ // Resolve instance once
94
+ let effectiveInstance;
95
+ try {
96
+ effectiveInstance = getEffectiveInstance(cfg);
97
+ } catch {
98
+ effectiveInstance = null;
99
+ }
146
100
 
147
- // Read-only profile check — block mutation commands on read-only profiles
148
- const activeProfileName = cfg.activeProfile || cfg.defaultProfile;
149
- if (activeProfileName && cfg.profiles[activeProfileName] && cfg.profiles[activeProfileName].read_only) {
150
- const skipReadOnlyCheck = ['help', 'version', 'setup', 'auth', 'profiles', 'profile', 'skill', undefined].includes(cmd);
151
- if (!skipReadOnlyCheck && isMutationCommand(argv)) {
152
- process.stderr.write(`\n🔒 Profile "${activeProfileName}" is read-only.\n`);
153
- process.stderr.write(` Mutation commands are disabled on this profile.\n`);
154
- const nonReadOnlyProfiles = Object.entries(cfg.profiles || {})
155
- .filter(([n, p]) => n !== activeProfileName && !p.read_only)
156
- .map(([n]) => n);
157
- if (nonReadOnlyProfiles.length > 0) {
158
- 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);
159
154
  }
160
- process.stderr.write('\n');
161
- 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);
162
181
  }
163
- }
164
182
 
165
- // Auto-check skill on every command (fire-and-forget, non-blocking)
166
- // Skipped under --json, --quiet, --no-skill-check, or env var
167
- const skipSkillCheck = ['help', 'version', 'completion', 'skill'].includes(cmd)
168
- || process.env.JSN_NO_SKILL_CHECK === '1'
169
- || argv['no-skill-check']
170
- || argv.json
171
- || argv.quiet;
172
- if (!skipSkillCheck) {
173
- // Fire check but don't await — it runs in background
174
- checkSkill().then(result => {
175
- if (result && !result.current && result.error) {
176
- process.stderr.write(`\n⚠ ${result.error}\n\n`);
177
- }
178
- }).catch(() => {});
179
- }
183
+ const cmd = (argv._[0] || '').toString();
180
184
 
181
- // Print context header for interactive terminals (at the TOP, before command output)
182
- if (!['help', 'version', 'completion', 'skill'].includes(cmd)) {
183
- await argv.app.printContextHeader(argv);
184
- }
185
- })
186
- .command(setupCmd(wrap))
187
- .command(authCmd(wrap))
188
- .command(profilesCmd(wrap))
189
- .command(recordsCmd(wrap))
190
- .command(catalogCmd(wrap))
191
- .command(incidentsCmd(wrap))
192
- .command(changesCmd(wrap))
193
- .command(requestsCmd(wrap))
194
- .command(tasksCmd(wrap))
195
- .command(usersCmd(wrap))
196
- .command(groupsCmd(wrap))
197
- .command(groupMembersCmd(wrap))
198
- .command(groupRolesCmd(wrap))
199
- .command(ticketsCmd(wrap))
200
- .command(devCmd(wrap))
201
- // AUTOMATION — things that react, run, or transform
202
- .command(flowsCmd(wrap))
203
- .command(actionsCmd(wrap))
204
- .command(rulesCmd(wrap))
205
- .command(scrapiCmd(wrap))
206
- .command(updateSetsCmd(wrap))
207
- .command(evalCmd(wrap))
208
- .command(restCmd(wrap))
209
- // ACCESS — who can see/do what
210
- .command(aclsCmd(wrap))
211
- .command(rolesCmd(wrap))
212
- .command(propertiesCmd(wrap))
213
- .command(scopesCmd(wrap))
214
- .command(privilegesCmd(wrap))
215
- .command(securitytypesCmd(wrap))
216
- .command(aliasesCmd(wrap))
217
- // UX — what users see/interact with
218
- .command(formsCmd(wrap))
219
- .command(listsCmd(wrap))
220
- .command(clientScriptsCmd(wrap))
221
- .command(uiActionsCmd(wrap))
222
- .command(uiPoliciesCmd(wrap))
223
- .command(spPagesCmd(wrap))
224
- .command(spWidgetsCmd(wrap))
225
- .command(uiPagesCmd(wrap))
226
- .command(appMenuCmd(wrap))
227
- .command(listcontrolsCmd(wrap))
228
- .command(viewsCmd(wrap))
229
- .command(uxscriptsCmd(wrap))
230
- .command(catalogscriptsCmd(wrap))
231
- .command(cataloguipoliciesCmd(wrap))
232
- // DATA — where data lives
233
- .command(tablesCmd(wrap))
234
- .command(columnsCmd(wrap))
235
- .command(includesCmd(wrap))
236
- .command(importCmd(wrap))
237
- .command(logsCmd(wrap))
238
- .command(relationshipsCmd(wrap))
239
- .command(appmodulesCmd(wrap))
240
- .command(skillCmd(wrap))
241
- .command(versionCmd(wrap))
242
- .demandCommand(1, 'You must specify a command')
243
- .help('help', 'Show help')
244
- .version(false)
245
- .strictCommands()
246
- .strictOptions(false)
247
- .fail((msg, err) => {
248
- if (err) throw err;
249
- // No command given → show custom grouped help instead of yargs error
250
- if (msg === 'You must specify a command') {
251
- // Check if --profile/-p was used without a value — show profile info instead
252
- const raw = process.argv.slice(2);
253
- const hasLoneProfile = raw.some((a, i, arr) => {
254
- if (a !== '--profile' && a !== '-p') return false;
255
- const next = arr[i + 1];
256
- return !next || next.startsWith('-');
257
- });
258
- if (hasLoneProfile) {
259
- const cfg = loadConfig();
260
- const activeName = cfg.activeProfile || cfg.defaultProfile;
261
- process.stdout.write('Profiles:\n');
262
- for (const [name, p] of Object.entries(cfg.profiles || {})) {
263
- const marker = name === activeName ? ' *' : ' ';
264
- 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);
265
312
  }
266
- process.stdout.write('\nUsage: jsn --profile <name> <command>\n');
267
- process.stdout.write(' jsn profiles use <name>\n');
313
+ process.stdout.write(renderHelp() + '\n');
268
314
  process.exit(0);
269
315
  }
270
- process.stdout.write(renderHelp());
271
- process.exit(0);
272
- }
273
- process.stderr.write(`Error: ${msg}\n`);
274
- process.exit(1);
275
- });
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();
@@ -255,6 +255,19 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
255
255
  })
256
256
 
257
257
  },
258
- handler: () => {}, // Handled by subcommands
258
+ handler: () => {
259
+ const singular = displayName.endsWith('s') ? displayName.slice(0, -1) : displayName;
260
+ const article = singular.startsWith('a') || singular.startsWith('e') || singular.startsWith('i') || singular.startsWith('o') || singular.startsWith('u') ? 'an' : 'a';
261
+ console.log(`Manage ${displayName} from the ${table} table.`);
262
+ console.log('');
263
+ console.log('Available subcommands:');
264
+ console.log(` list List ${displayName}`);
265
+ console.log(` show <number or sys_id> Show ${article} ${singular} by number or sys_id`);
266
+ console.log(` create Create ${article} ${singular}`);
267
+ console.log(` update <identifier> Update ${article} ${singular}`);
268
+ console.log(` delete <identifier> Delete ${article} ${singular}`);
269
+ console.log('');
270
+ console.log(`Run "jsn ${displayName} <command> --help" for details.`);
271
+ }, // Default handler
259
272
  };
260
273
  }
@@ -302,6 +302,15 @@ Examples:
302
302
  }),
303
303
  });
304
304
  },
305
- handler: () => {},
305
+ handler: () => {
306
+ console.log('Manage OAuth authentication for ServiceNow instances.');
307
+ console.log('');
308
+ console.log('Available subcommands:');
309
+ console.log(' login <url> Login with OAuth (supports --print-url, --code for CI/CD)');
310
+ console.log(' status Show auth status for the active profile');
311
+ console.log(' refresh Refresh the access token');
312
+ console.log('');
313
+ console.log('Run "jsn auth <command> --help" for details.');
314
+ },
306
315
  };
307
316
  }