@jacebenson/jsn 0.0.10 โ†’ 1.0.2

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 (41) hide show
  1. package/README.md +7 -49
  2. package/bin/jsn.js +57 -2
  3. package/package.json +28 -32
  4. package/scripts/install.sh +227 -0
  5. package/scripts/npm-install.js +235 -0
  6. package/scripts/pre-commit-check.sh +61 -0
  7. package/src/app.js +0 -157
  8. package/src/auth.js +0 -283
  9. package/src/cli.js +0 -144
  10. package/src/commands/_ticket.js +0 -256
  11. package/src/commands/auth.js +0 -62
  12. package/src/commands/changes.js +0 -7
  13. package/src/commands/dev/_generic.js +0 -223
  14. package/src/commands/dev/_simple.js +0 -89
  15. package/src/commands/dev/eval.js +0 -17
  16. package/src/commands/dev/flows.js +0 -528
  17. package/src/commands/dev/forms.js +0 -313
  18. package/src/commands/dev/lists.js +0 -233
  19. package/src/commands/dev/logs.js +0 -51
  20. package/src/commands/dev/rest.js +0 -64
  21. package/src/commands/dev/scopes.js +0 -96
  22. package/src/commands/dev/updatesets.js +0 -97
  23. package/src/commands/dev.js +0 -53
  24. package/src/commands/groupmembers.js +0 -39
  25. package/src/commands/grouproles.js +0 -39
  26. package/src/commands/groups.js +0 -57
  27. package/src/commands/incidents.js +0 -7
  28. package/src/commands/profiles.js +0 -79
  29. package/src/commands/records.js +0 -137
  30. package/src/commands/requests.js +0 -7
  31. package/src/commands/setup.js +0 -39
  32. package/src/commands/tasks.js +0 -7
  33. package/src/commands/tickets.js +0 -121
  34. package/src/commands/users.js +0 -57
  35. package/src/commands/version.js +0 -25
  36. package/src/config.js +0 -154
  37. package/src/context.js +0 -62
  38. package/src/errors.js +0 -101
  39. package/src/helpers.js +0 -60
  40. package/src/output.js +0 -410
  41. package/src/sdk.js +0 -357
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # pre-commit-check.sh - Run this before committing to check architecture compliance
3
+
4
+ echo "๐Ÿ” Checking architecture patterns..."
5
+
6
+ # Check for forbidden SDK patterns
7
+ FORBIDDEN_PATTERNS=(
8
+ "func.*Client.*ListForm"
9
+ "func.*Client.*ListList"
10
+ "func.*Client.*GetSP"
11
+ "func.*Client.*ListSP"
12
+ )
13
+
14
+ ERRORS=0
15
+
16
+ for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
17
+ matches=$(grep -r "$pattern" internal/sdk/*.go 2>/dev/null | grep -v "_test.go" | grep -v "^Binary")
18
+ if [ ! -z "$matches" ]; then
19
+ echo "โŒ Found forbidden SDK pattern: $pattern"
20
+ echo "$matches"
21
+ ERRORS=$((ERRORS + 1))
22
+ fi
23
+ done
24
+
25
+ # Check that commands don't import old SDK types
26
+ BAD_IMPORTS=(
27
+ "sdk.FormSection"
28
+ "sdk.FormElement"
29
+ "sdk.ListLayout"
30
+ "sdk.ListElement"
31
+ "sdk.SPPage"
32
+ "sdk.SPWidgetInstance"
33
+ )
34
+
35
+ for pattern in "${BAD_IMPORTS[@]}"; do
36
+ matches=$(grep -r "$pattern" internal/commands/**/*.go 2>/dev/null)
37
+ if [ ! -z "$matches" ]; then
38
+ echo "โŒ Command using SDK type instead of local type: $pattern"
39
+ echo "$matches"
40
+ ERRORS=$((ERRORS + 1))
41
+ fi
42
+ done
43
+
44
+ # Run architecture tests
45
+ echo "๐Ÿงช Running architecture tests..."
46
+ if ! go test ./internal/sdk -run TestNoSDKHelperMethods -run TestCommandsUseDirectSDKList > /dev/null 2>&1; then
47
+ echo "โŒ Architecture tests failed"
48
+ go test ./internal/sdk -v -run "TestNoSDKHelperMethods|TestCommandsUseDirectSDKList"
49
+ ERRORS=$((ERRORS + 1))
50
+ fi
51
+
52
+ if [ $ERRORS -eq 0 ]; then
53
+ echo "โœ… All architecture checks passed!"
54
+ exit 0
55
+ else
56
+ echo ""
57
+ echo "โš ๏ธ Architecture violations found!"
58
+ echo " Remember: Commands should call app.SDK.List() directly with local types."
59
+ echo " See internal/commands/dev/forms.go for the correct pattern."
60
+ exit 1
61
+ fi
package/src/app.js DELETED
@@ -1,157 +0,0 @@
1
- // App context: bundles config, auth, SDK, output, and runtime context
2
-
3
- import { AuthManager } from './auth.js';
4
- import { SDKClient } from './sdk.js';
5
- import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
6
- import { getEffectiveInstance } from './config.js';
7
- import { extractProfileName } from './helpers.js';
8
- import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
9
- import { errUsage, errAuth } from './errors.js';
10
- import process from 'node:process';
11
-
12
-
13
- export class App {
14
- constructor(cfg) {
15
- this.config = cfg;
16
- this.auth = new AuthManager(this);
17
- this.output = new OutputWriter({ format: resolveFormat(cfg.format) });
18
- this.sdk = null;
19
-
20
- const instance = getEffectiveInstance(cfg);
21
- if (instance) {
22
- this.sdk = new SDKClient(instance, this.auth);
23
- }
24
-
25
- this.context = {
26
- profileName: '',
27
- username: '',
28
- scope: '',
29
- updateSet: '',
30
- };
31
-
32
- this.loadContext();
33
- }
34
-
35
- loadContext() {
36
- const instance = getEffectiveInstance(this.config);
37
- if (!instance) return;
38
- this.context.profileName = extractProfileName(instance);
39
- for (const [name, profile] of Object.entries(this.config.profiles || {})) {
40
- if (profile.instance_url === instance) {
41
- this.context.profileName = name;
42
- this.context.username = profile.username || '';
43
- break;
44
- }
45
- }
46
- }
47
-
48
- getEffectiveInstance() {
49
- return getEffectiveInstance(this.config);
50
- }
51
-
52
- async printContextHeader() {
53
- if (!this.getEffectiveInstance() || !this.sdk) return;
54
- if (process.env.JSN_NO_HEADER) return;
55
- if (this.output.getFormat() === FormatJSON || this.output.getFormat() === FormatQuiet) return;
56
-
57
- let userDisplayName = 'Unknown';
58
- let userSysID = '';
59
-
60
- try {
61
- const user = await getCurrentUser(this.sdk);
62
- if (user) {
63
- userDisplayName = user.name || user.user_name;
64
- userSysID = user.sys_id;
65
- this.context.username = userDisplayName;
66
- }
67
- } catch {
68
- // ignore
69
- }
70
-
71
- let displayUserName = userDisplayName;
72
- if (displayUserName.length > 10) {
73
- displayUserName = displayUserName.slice(0, 6) + '...';
74
- }
75
-
76
- let scope = 'global';
77
- if (userSysID) {
78
- try {
79
- const app = await getCurrentApplication(this.sdk, userSysID);
80
- if (app && app.scope) scope = app.scope;
81
- } catch {
82
- // ignore
83
- }
84
- }
85
- this.context.scope = scope;
86
-
87
- let updateSet = 'Default';
88
- let updateSetSysID = '';
89
- if (userSysID) {
90
- try {
91
- const us = await getCurrentUpdateSet(this.sdk, userSysID);
92
- if (us && us.name && us.name !== '-') {
93
- updateSet = us.name;
94
- updateSetSysID = us.sys_id;
95
- }
96
- } catch {
97
- // ignore
98
- }
99
- }
100
- this.context.updateSet = updateSet;
101
-
102
- const instance = this.getEffectiveInstance();
103
- const instanceLink = instance;
104
- const userLink = `${instance}/sys_user_list.do?sysparm_query=sys_id=${userSysID}`;
105
- const scopeLink = `${instance}/sys_scope.do?sysparm_query=scope=${scope}`;
106
- const updateSetLink = updateSetSysID
107
- ? `${instance}/sys_update_set.do?sys_id=${updateSetSysID}`
108
- : `${instance}/sys_update_set_list.do`;
109
-
110
- const scopeFormatted = `[${scope}]`;
111
-
112
- process.stderr.write('# Use `jsn updateset use` or `jsn scope use` to change scope/updateset\n');
113
- process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
114
-
115
- const profileStr = `]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}]8;;\x07`;
116
- const userStr = `]8;;${userLink}\x07${String(displayUserName).padEnd(9)}]8;;\x07`;
117
- const scopeStr = `]8;;${scopeLink}\x07${String(scopeFormatted).padEnd(17)}]8;;\x07`;
118
- const updateSetStr = `]8;;${updateSetLink}\x07${updateSet}]8;;\x07`;
119
-
120
- process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
121
- }
122
-
123
- ok(data, opts = {}) {
124
- this.output.ok(data, opts);
125
- }
126
-
127
- err(error) {
128
- this.output.err(error);
129
- }
130
-
131
- isInteractive() {
132
- return process.stdout.isTTY === true;
133
- }
134
-
135
- requireInstance() {
136
- if (!this.getEffectiveInstance()) {
137
- throw errUsage('Instance URL required. Set via --instance flag, SERVICENOW_INSTANCE_URL env, or config file.');
138
- }
139
- }
140
-
141
- requireAuth() {
142
- if (!this.auth.isAuthenticated()) {
143
- throw errAuth('Not authenticated');
144
- }
145
- }
146
- }
147
-
148
- function resolveFormat(fmt) {
149
- switch (fmt) {
150
- case 'json': return FormatJSON;
151
- case 'markdown':
152
- case 'md': return FormatMarkdown;
153
- case 'quiet': return FormatQuiet;
154
- case 'styled': return FormatStyled;
155
- default: return FormatAuto;
156
- }
157
- }
package/src/auth.js DELETED
@@ -1,283 +0,0 @@
1
- // OAuth 2.0 with PKCE authentication
2
-
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import crypto from 'node:crypto';
6
- import readline from 'node:readline';
7
- import { globalConfigDir, normalizeInstanceURL } from './config.js';
8
- import { errAuth } from './errors.js';
9
-
10
- const DEFAULT_OAUTH_CLIENT_ID = '543e5655f77746a28228c6009a599dfb';
11
- const REDIRECT_URI = '/sdk-oauth.do';
12
-
13
- function credentialsPath(instance) {
14
- const dir = path.join(globalConfigDir(), 'credentials');
15
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
16
- const filename = Buffer.from(instance).toString('base64url') + '.json';
17
- return path.join(dir, filename);
18
- }
19
-
20
- function getOAuthClientID() {
21
- return process.env.SERVICENOW_OAUTH_CLIENT_ID || DEFAULT_OAUTH_CLIENT_ID;
22
- }
23
-
24
- function generatePKCE() {
25
- const verifier = crypto.randomBytes(32).toString('base64url');
26
- const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
27
- const state = crypto.randomBytes(16).toString('base64url');
28
- return { code_verifier: verifier, code_challenge: challenge, state };
29
- }
30
-
31
- function buildAuthURL(instanceURL, clientID, pkce) {
32
- const u = new URL('/oauth_auth.do', instanceURL);
33
- u.searchParams.set('response_type', 'code');
34
- u.searchParams.set('client_id', clientID);
35
- u.searchParams.set('redirect_uri', REDIRECT_URI);
36
- u.searchParams.set('state', pkce.state);
37
- u.searchParams.set('code_challenge', pkce.code_challenge);
38
- u.searchParams.set('code_challenge_method', 'S256');
39
- u.searchParams.set('scope', 'openid');
40
- return u.toString();
41
- }
42
-
43
- function loadCredentials(instance) {
44
- try {
45
- const data = fs.readFileSync(credentialsPath(instance), 'utf-8');
46
- return JSON.parse(data);
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- function saveCredentials(instance, creds) {
53
- fs.writeFileSync(credentialsPath(instance), JSON.stringify(creds, null, 2), { mode: 0o600 });
54
- }
55
-
56
- function deleteCredentials(instance) {
57
- try {
58
- fs.unlinkSync(credentialsPath(instance));
59
- } catch {
60
- // ignore
61
- }
62
- }
63
-
64
- function askHidden(promptText) {
65
- return new Promise((resolve) => {
66
- const rl = readline.createInterface({
67
- input: process.stdin,
68
- output: process.stdout,
69
- });
70
-
71
- const stdin = process.stdin;
72
- const stdout = process.stdout;
73
-
74
- if (!stdin.isTTY) {
75
- rl.question(promptText, (answer) => {
76
- rl.close();
77
- resolve(answer.trim());
78
- });
79
- return;
80
- }
81
-
82
- stdout.write(promptText);
83
-
84
- stdin.setRawMode(true);
85
- stdin.resume();
86
- stdin.setEncoding('utf-8');
87
-
88
- let input = '';
89
- const onData = (key) => {
90
- if (key === '\r' || key === '\n') {
91
- stdin.removeListener('data', onData);
92
- stdin.setRawMode(false);
93
- stdin.pause();
94
- stdout.write('\n');
95
- rl.close();
96
- resolve(input);
97
- } else if (key === '\u0003') {
98
- process.exit();
99
- } else if (key === '\u007f') {
100
- if (input.length > 0) {
101
- input = input.slice(0, -1);
102
- stdout.write('\b \b');
103
- }
104
- } else {
105
- input += key;
106
- stdout.write('*');
107
- }
108
- };
109
- stdin.on('data', onData);
110
- });
111
- }
112
-
113
- export class AuthManager {
114
- constructor(configProvider) {
115
- this.configProvider = configProvider;
116
- this.httpClient = { timeout: 30000 };
117
- }
118
-
119
- isAuthenticated() {
120
- if (process.env.SERVICENOW_OAUTH_TOKEN) return true;
121
- const instance = this.configProvider.getEffectiveInstance();
122
- if (!instance) return false;
123
- try {
124
- this.getCredentialsFor(instance);
125
- return true;
126
- } catch {
127
- return false;
128
- }
129
- }
130
-
131
- isAuthenticatedFor(instance) {
132
- if (!instance) return false;
133
- const creds = loadCredentials(instance);
134
- if (!creds) return false;
135
- if (creds.expires_at && Date.now() >= creds.expires_at * 1000) return false;
136
- return !!creds.access_token;
137
- }
138
-
139
- async getCredentials() {
140
- if (process.env.SERVICENOW_OAUTH_TOKEN) {
141
- return { auth_method: 'oauth', access_token: process.env.SERVICENOW_OAUTH_TOKEN };
142
- }
143
- const instance = this.configProvider.getEffectiveInstance();
144
- if (!instance) {
145
- throw errAuth('No instance configured');
146
- }
147
- return this.getCredentialsFor(instance);
148
- }
149
-
150
- getCredentialsFor(instance) {
151
- const creds = loadCredentials(instance);
152
- if (!creds) {
153
- throw errAuth(`Not authenticated for ${instance}`);
154
- }
155
- // Check expiry โ€” refresh if less than 5 minutes remaining
156
- if (creds.expires_at && Date.now() >= (creds.expires_at - 300) * 1000) {
157
- if (creds.refresh_token) {
158
- return this.refreshToken(instance, creds);
159
- }
160
- throw errAuth('Token expired, please login again');
161
- }
162
- return creds;
163
- }
164
-
165
- async login(instanceURL) {
166
- instanceURL = normalizeInstanceURL(instanceURL);
167
- const clientID = getOAuthClientID();
168
- const pkce = generatePKCE();
169
- const authURL = buildAuthURL(instanceURL, clientID, pkce);
170
-
171
- console.log();
172
- console.log('Opening browser for OAuth authentication...');
173
- console.log('If the browser does not open automatically, visit:');
174
- console.log(authURL);
175
- console.log();
176
-
177
- // Try to open browser
178
- const open = (await import('node:child_process')).spawn;
179
- const platform = process.platform;
180
- let cmd, args;
181
- if (platform === 'darwin') {
182
- cmd = 'open';
183
- args = [authURL];
184
- } else if (platform === 'win32') {
185
- cmd = 'cmd';
186
- args = ['/c', 'start', authURL];
187
- } else {
188
- cmd = 'xdg-open';
189
- args = [authURL];
190
- }
191
- const child = open(cmd, args, { detached: true, stdio: 'ignore' });
192
- child.on('error', () => {
193
- // Browser open command not available โ€” user will open the URL manually
194
- });
195
- child.unref();
196
-
197
- console.log('After authenticating in the browser, copy the authorization code shown on the page.');
198
- console.log('(input is hidden for security โ€” just paste and press Enter)');
199
- console.log();
200
-
201
- const authCode = await askHidden('Authorization code (hidden on paste for security): ');
202
- const code = authCode.trim();
203
- if (!code) {
204
- throw errAuth('Authorization code is required');
205
- }
206
-
207
- console.log('\nExchanging authorization code for tokens...');
208
- const newCreds = await this.exchangeCode(instanceURL, clientID, code, pkce);
209
- saveCredentials(instanceURL, newCreds);
210
- return newCreds;
211
- }
212
-
213
- async exchangeCode(instanceURL, clientID, code, pkce) {
214
- const tokenURL = `${instanceURL.replace(/\/$/, '')}/oauth_token.do`;
215
- const body = new URLSearchParams();
216
- body.set('grant_type', 'authorization_code');
217
- body.set('client_id', clientID);
218
- body.set('code', code);
219
- body.set('redirect_uri', REDIRECT_URI);
220
- body.set('code_verifier', pkce.code_verifier);
221
-
222
- const resp = await fetch(tokenURL, {
223
- method: 'POST',
224
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
225
- body: body.toString(),
226
- });
227
-
228
- const text = await resp.text();
229
- if (!resp.ok) {
230
- throw errAuth(`Token exchange failed (status ${resp.status}): ${text}`);
231
- }
232
-
233
- const tokenResp = JSON.parse(text);
234
- const expiresAt = tokenResp.expires_in ? Math.floor(Date.now() / 1000) + tokenResp.expires_in : 0;
235
- return {
236
- auth_method: 'oauth',
237
- access_token: tokenResp.access_token,
238
- refresh_token: tokenResp.refresh_token,
239
- expires_at: expiresAt,
240
- created_at: Math.floor(Date.now() / 1000),
241
- };
242
- }
243
-
244
- async refreshToken(instance, creds) {
245
- const tokenURL = `${instance.replace(/\/$/, '')}/oauth_token.do`;
246
- const clientID = getOAuthClientID();
247
- const body = new URLSearchParams();
248
- body.set('grant_type', 'refresh_token');
249
- body.set('client_id', clientID);
250
- body.set('refresh_token', creds.refresh_token);
251
-
252
- const resp = await fetch(tokenURL, {
253
- method: 'POST',
254
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
255
- body: body.toString(),
256
- });
257
-
258
- if (!resp.ok) {
259
- const text = await resp.text();
260
- throw errAuth(`Token refresh failed: ${text}`);
261
- }
262
-
263
- const tokenResp = await resp.json();
264
- const newCreds = {
265
- auth_method: 'oauth',
266
- access_token: tokenResp.access_token,
267
- refresh_token: tokenResp.refresh_token,
268
- created_at: Math.floor(Date.now() / 1000),
269
- };
270
- if (tokenResp.expires_in) {
271
- newCreds.expires_at = Math.floor(Date.now() / 1000) + tokenResp.expires_in;
272
- }
273
- saveCredentials(instance, newCreds);
274
- return newCreds;
275
- }
276
-
277
- logout(instance) {
278
- if (!instance) {
279
- throw errAuth('No instance specified');
280
- }
281
- deleteCredentials(instance);
282
- }
283
- }
package/src/cli.js DELETED
@@ -1,144 +0,0 @@
1
- // Root CLI using yargs
2
-
3
- import yargs from 'yargs';
4
- import { hideBin } from 'yargs/helpers';
5
- import process from 'node:process';
6
- import { loadConfig, getEffectiveInstance } from './config.js';
7
- import { App } from './app.js';
8
-
9
- // Command modules
10
- import { setupCmd } from './commands/setup.js';
11
- import { authCmd } from './commands/auth.js';
12
- import { profilesCmd } from './commands/profiles.js';
13
- import { recordsCmd } from './commands/records.js';
14
- import { incidentsCmd } from './commands/incidents.js';
15
- import { changesCmd } from './commands/changes.js';
16
- import { requestsCmd } from './commands/requests.js';
17
- import { tasksCmd } from './commands/tasks.js';
18
- import { usersCmd } from './commands/users.js';
19
- import { groupsCmd } from './commands/groups.js';
20
- import { groupMembersCmd } from './commands/groupmembers.js';
21
- import { groupRolesCmd } from './commands/grouproles.js';
22
- import { ticketsCmd } from './commands/tickets.js';
23
- import { versionCmd } from './commands/version.js';
24
- import { devCmd } from './commands/dev.js';
25
-
26
- function wrap(handler) {
27
- return async (argv) => {
28
- try {
29
- const app = argv.app;
30
- if (!app) {
31
- process.stderr.write('Error: App context not initialized.\n');
32
- process.exit(1);
33
- }
34
- await handler(argv, app);
35
- } catch (err) {
36
- const app = argv.app;
37
- if (app) {
38
- app.err(err);
39
- } else {
40
- process.stderr.write(`Error: ${err.message || err}\n`);
41
- }
42
- process.exit(1);
43
- }
44
- };
45
- }
46
-
47
- export const cli = yargs(hideBin(process.argv))
48
- .scriptName('jsn')
49
- .usage('Usage: $0 <command> [options]')
50
- .option('instance', {
51
- describe: 'ServiceNow instance URL (e.g., https://dev12345.service-now.com)',
52
- type: 'string',
53
- global: true,
54
- })
55
- .option('profile', {
56
- alias: 'p',
57
- describe: 'Configuration profile to use',
58
- type: 'string',
59
- global: true,
60
- })
61
- .option('format', {
62
- describe: 'Output format: auto, json, markdown, styled, quiet',
63
- type: 'string',
64
- global: true,
65
- })
66
- .option('json', {
67
- describe: 'Output in JSON format',
68
- type: 'boolean',
69
- global: true,
70
- })
71
- .option('quiet', {
72
- alias: 'q',
73
- describe: 'Output only data, no envelope',
74
- type: 'boolean',
75
- global: true,
76
- })
77
- .option('styled', {
78
- describe: 'Force styled output',
79
- type: 'boolean',
80
- global: true,
81
- })
82
- .option('markdown', {
83
- describe: 'Output in Markdown format',
84
- type: 'boolean',
85
- global: true,
86
- })
87
- .middleware((argv) => {
88
- // Determine format from flags
89
- let format = 'auto';
90
- if (argv.json) format = 'json';
91
- else if (argv.quiet) format = 'quiet';
92
- else if (argv.styled) format = 'styled';
93
- else if (argv.markdown) format = 'markdown';
94
- else if (argv.format) format = argv.format;
95
-
96
- const cfg = loadConfig({
97
- instance: argv.instance,
98
- profile: argv.profile,
99
- format,
100
- });
101
-
102
- argv.app = new App(cfg);
103
-
104
- // Check auth for non-auth commands
105
- const cmd = argv._[0];
106
- const skipAuth = ['help', 'version', 'setup', 'auth', undefined].includes(cmd);
107
- if (!skipAuth) {
108
- const instance = getEffectiveInstance(cfg);
109
- if (!argv.app.auth.isAuthenticated() && instance) {
110
- process.stderr.write(`\nโš ๏ธ Not authenticated to ${instance}\n\n`);
111
- process.stderr.write('To get started, run:\n');
112
- process.stderr.write(' jsn setup # Interactive setup\n');
113
- process.stderr.write(` jsn auth login ${instance} # Login to instance\n\n`);
114
- }
115
- }
116
-
117
- // Print context header for interactive terminals
118
- if (!['help', 'version', 'completion'].includes(cmd)) {
119
- argv.app.printContextHeader();
120
- }
121
- })
122
- .command(setupCmd(wrap))
123
- .command(authCmd(wrap))
124
- .command(profilesCmd(wrap))
125
- .command(recordsCmd(wrap))
126
- .command(incidentsCmd(wrap))
127
- .command(changesCmd(wrap))
128
- .command(requestsCmd(wrap))
129
- .command(tasksCmd(wrap))
130
- .command(usersCmd(wrap))
131
- .command(groupsCmd(wrap))
132
- .command(groupMembersCmd(wrap))
133
- .command(groupRolesCmd(wrap))
134
- .command(ticketsCmd(wrap))
135
- .command(devCmd(wrap))
136
- .command(versionCmd(wrap))
137
- .demandCommand(1, 'You must specify a command')
138
- .help('help', 'Show help')
139
- .version(false)
140
- .strictCommands()
141
- .strictOptions(false)
142
- .epilogue('TIPS\n'
143
- + ' --query is available on every list command (e.g. "incidents list --query priority=1")\n'
144
- + ' Use "jsn <command> --help" for details, or "jsn <command> list --help" for list options');