@jacebenson/jsn 4.3.13 → 4.4.0-dev.114.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.
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
@@ -0,0 +1,234 @@
1
+ import { buildRecordPayload, confirmDelete, getStringField, quoteShellArg, verifyWriteBack } from '../helpers.js';
2
+ import { createRecord, updateRecord, deleteRecord } from '../records-engine.js';
3
+ import { unwrapSysId } from '../resolve-record.js';
4
+ import { getCurrentApplication, getCurrentUser } from '../context.js';
5
+ import { declareCapabilities } from '../capabilities.js';
6
+ import { errAPI, errUsage } from '../errors.js';
7
+ import { CHECK_TABLES, active, raw, referenceId, boundedLimit, scanList, resolveScanRecord,
8
+ validateScanPayload, checkKind, prepareScan, startScan, scanStatus, scanResults, cancelScan } from '../scan.js';
9
+
10
+ declareCapabilities('scan', { mutationSubcommands: [
11
+ ...['checks', 'suites'].flatMap(group => ['create', 'update', 'delete', 'activate', 'deactivate'].map(verb => `${group} ${verb}`)),
12
+ 'suites add-check', 'suites remove-check', 'run', 'cancel',
13
+ ] });
14
+
15
+ const hint = (action, cmd, description) => ({ action, cmd, description });
16
+ function report(app, argv, data, options) {
17
+ const connection = argv.profile ? ` --profile ${quoteShellArg(argv.profile)}`
18
+ : argv.instance ? ` --instance ${quoteShellArg(argv.instance)}` : '';
19
+ app.ok(data, { ...options, breadcrumbs: options.breadcrumbs.map(item => ({ ...item, cmd: item.cmd + connection })) });
20
+ }
21
+ const rootHints = () => [hint('checks', 'jsn scan checks list', 'Browse available checks'),
22
+ hint('suites', 'jsn scan suites list', 'Browse suites'), hint('run', 'jsn scan run --help', 'Choose an explicit scan target')];
23
+ const itemHints = (group, record) => {
24
+ const id = unwrapSysId(record);
25
+ return [hint('list', `jsn scan ${group} list`, `Browse ${group}`),
26
+ hint('show', `jsn scan ${group} show ${id}`, 'Inspect this definition'),
27
+ hint('update', `jsn scan ${group} update ${id} --data-file ${group === 'checks' ? 'check' : 'suite'}.json`, 'Update selected fields'),
28
+ hint(active(record) ? 'deactivate' : 'activate', `jsn scan ${group} ${active(record) ? 'deactivate' : 'activate'} ${id}`, 'Change eligibility without starting a scan'),
29
+ ...(group === 'suites' ? [hint('add-check', `jsn scan suites add-check ${id} --check '<check-id-or-name>'`, 'Add an existing check'),
30
+ hint('run', `jsn scan run --suite ${id} --update-set '<update-set>'`, 'Run against an explicit update set')] :
31
+ [hint('suites', 'jsn scan suites list', 'Choose a suite for this check')]),
32
+ hint('delete', `jsn scan ${group} delete ${id}`, 'Delete this definition after confirmation')];
33
+ };
34
+
35
+ function executionHints(data) {
36
+ const id = data.execution_id;
37
+ return [hint('status', `jsn scan status ${id}`, 'Read execution progress'),
38
+ hint('results', `jsn scan results ${id}`, 'Inspect findings and recorded check executions'),
39
+ ...(!data.terminal ? [hint('cancel', `jsn scan cancel ${id}`, 'Request cancellation')] : []),
40
+ ...(data.reusable_target ? [hint('rerun', `jsn scan run --previous ${id}${data.rerun_full ? ' --full' : ''}`, 'Rerun the previous target with current check definitions')] : []),
41
+ hint('checks', 'jsn scan checks list', 'Review available checks')];
42
+ }
43
+
44
+ const payloadOptions = y => y.option('data', { type: 'string', describe: 'JSON fields' })
45
+ .option('data-file', { type: 'string', describe: 'Read JSON fields from a file (recommended for scripts)' })
46
+ .option('data-stdin', { type: 'boolean', describe: 'Read JSON fields from stdin' })
47
+ .option('field', { alias: 'f', type: 'array', describe: 'Set field=value; repeatable' });
48
+ const lookupOptions = y => y.positional('identifier', { type: 'string', describe: 'Unique name or sys_id' });
49
+
50
+ async function guardScope(app, record) {
51
+ const scopeId = referenceId(record.sys_scope) || raw(record.sys_scope);
52
+ if (!scopeId) throw errUsage('The definition scope is unreadable; inspect permissions before changing it.');
53
+ const user = await getCurrentUser(app.sdk);
54
+ if (!user) throw errUsage('Could not verify the current scope.');
55
+ const scope = await getCurrentApplication(app.sdk, user.sys_id, { unknownWhenMissing: true });
56
+ if (scope.appSysId !== scopeId && scope.scope !== scopeId) {
57
+ throw errUsage(`Select the definition scope before changing it: jsn scopes set ${quoteShellArg(scopeId)}`);
58
+ }
59
+ }
60
+
61
+ async function verify(app, table, id, data) {
62
+ if (!id) throw errAPI(200, 'The write returned no sys_id. Inspect the instance before retrying.');
63
+ const mismatches = await verifyWriteBack(app, table, id, data);
64
+ if (mismatches.length) throw errAPI(200, `Write-back verification failed for ${table}/${id}: ${mismatches.map(item => item.field).join(', ')}. The write may have succeeded partially; inspect before retrying.`);
65
+ }
66
+
67
+ function definitions(group, wrap) {
68
+ const suite = group === 'suites';
69
+ const baseTable = suite ? 'scan_check_suite' : 'scan_check';
70
+ const emit = (app, argv, record, summary) => report(app, argv, record, { summary, breadcrumbs: itemHints(group, record) });
71
+ return {
72
+ command: `${group} [subcommand]`,
73
+ describe: suite ? 'Manage scan suites and their check memberships' : 'Manage typed Instance Scan check definitions',
74
+ builder: y => {
75
+ y.command({ command: 'list', describe: `List ${group}`,
76
+ builder: p => {
77
+ p.option('query', { type: 'string', describe: 'Encoded query' })
78
+ .option('limit', { type: 'number', default: 50, describe: 'Maximum records (1–200)' })
79
+ .option('offset', { type: 'number', default: 0, describe: 'Pagination offset' });
80
+ return suite ? p : p.option('type', { choices: Object.keys(CHECK_TABLES), type: 'string', describe: 'Filter by check type' });
81
+ }, handler: wrap(async (argv, app) => {
82
+ const table = suite ? baseTable : (argv.type ? CHECK_TABLES[argv.type] : baseTable);
83
+ const records = await scanList(app.sdk, table, { limit: argv.limit, offset: argv.offset, query: argv.query,
84
+ fields: ['sys_id', 'name', 'active', ...(suite ? ['description'] : ['sys_class_name', 'short_description'])] });
85
+ report(app, argv, { table, records, count: records.length, limit: argv.limit, offset: argv.offset,
86
+ more_available: records.length === argv.limit ? null : false }, {
87
+ summary: `${records.length} scan ${group}`, breadcrumbs: [
88
+ ...records.slice(0, 5).map(record => hint('show', `jsn scan ${group} show ${unwrapSysId(record)}`, `Inspect ${getStringField(record, 'name')}`)),
89
+ hint('create', `jsn scan ${group} create${suite ? '' : ' --type table'} --data-file ${suite ? 'suite' : 'check'}.json`, 'Create an inactive definition'),
90
+ ...(records.length === argv.limit ? [hint('next', `jsn scan ${group} list --offset ${argv.offset + argv.limit} --limit ${argv.limit}${argv.type ? ` --type ${argv.type}` : ''}${argv.query ? ` --query ${quoteShellArg(argv.query)}` : ''}`, 'Next page')] : []),
91
+ ],
92
+ });
93
+ }) });
94
+ y.command({ command: 'show <identifier>', describe: `Inspect a ${suite ? 'suite and its members' : 'check'}`, builder: lookupOptions,
95
+ handler: wrap(async (argv, app) => {
96
+ const record = await resolveScanRecord(app.sdk, baseTable, argv.identifier);
97
+ if (suite) {
98
+ const members = await scanList(app.sdk, 'scan_check_suite_check', { limit: 200, query: `suite=${unwrapSysId(record)}^ORDERBYsys_id`, fields: ['sys_id', 'check'] });
99
+ record.members = members;
100
+ record.members_may_be_truncated = members.length === 200;
101
+ }
102
+ emit(app, argv, record, `Scan ${suite ? 'suite' : 'check'}: ${getStringField(record, 'name')}`);
103
+ }) });
104
+ y.command({ command: 'create', describe: `Create an inactive ${suite ? 'suite' : 'typed check'} unless active is explicitly supplied`,
105
+ builder: p => suite ? payloadOptions(p) : payloadOptions(p).option('type', { choices: Object.keys(CHECK_TABLES), type: 'string', demandOption: true, describe: 'Check type' }),
106
+ handler: wrap(async (argv, app) => {
107
+ const data = validateScanPayload(argv.type, buildRecordPayload(argv), { create: true, suite });
108
+ const table = suite ? baseTable : CHECK_TABLES[argv.type];
109
+ const record = await createRecord(app.sdk, { table, data });
110
+ await verify(app, table, unwrapSysId(record), data);
111
+ emit(app, argv, record, `Created scan ${suite ? 'suite' : 'check'}`);
112
+ }) });
113
+ for (const operation of ['update', 'activate', 'deactivate']) y.command({
114
+ command: `${operation} <identifier>`, describe: `${operation} a scan ${suite ? 'suite' : 'check'} without running it`,
115
+ builder: p => operation === 'update' ? payloadOptions(lookupOptions(p)) : lookupOptions(p),
116
+ handler: wrap(async (argv, app) => {
117
+ const record = await resolveScanRecord(app.sdk, baseTable, argv.identifier);
118
+ const kind = suite ? null : checkKind(record);
119
+ const patch = operation === 'update' ? validateScanPayload(kind, buildRecordPayload(argv), { suite, current: record }) : { active: operation === 'activate' };
120
+ if (!suite && operation === 'activate') validateScanPayload(kind, {
121
+ name: raw(record.name), script: raw(record.script), ...(kind === 'table' ? { table: raw(record.table), advanced: raw(record.advanced) } : {}),
122
+ ...(kind === 'column' ? { column_type: raw(record.column_type) } : {}),
123
+ }, { create: true });
124
+ await guardScope(app, record);
125
+ const table = suite ? baseTable : CHECK_TABLES[kind];
126
+ const updated = await updateRecord(app.sdk, { table, record, data: patch });
127
+ await verify(app, table, unwrapSysId(record), patch);
128
+ emit(app, argv, updated, `Updated scan ${suite ? 'suite' : 'check'}`);
129
+ }),
130
+ });
131
+ y.command({ command: 'delete <identifier>', describe: `Delete a ${suite ? 'suite' : 'check'} definition after confirmation`, builder: lookupOptions,
132
+ handler: wrap(async (argv, app) => {
133
+ const record = await resolveScanRecord(app.sdk, baseTable, argv.identifier);
134
+ await guardScope(app, record);
135
+ await confirmDelete(app, argv, `Delete scan ${suite ? 'suite' : 'check'} "${getStringField(record, 'name')}"?`);
136
+ const id = await deleteRecord(app.sdk, { table: suite ? baseTable : CHECK_TABLES[checkKind(record)], record });
137
+ report(app, argv, { sys_id: id, deleted: true }, { summary: 'Deleted scan definition', breadcrumbs: [hint('list', `jsn scan ${group} list`, 'Browse remaining definitions')] });
138
+ }) });
139
+ if (suite) for (const operation of ['add-check', 'remove-check']) y.command({
140
+ command: `${operation} <identifier>`, describe: `${operation === 'add-check' ? 'Add a check to' : 'Remove a check from'} a suite`,
141
+ builder: p => lookupOptions(p).option('check', { type: 'string', demandOption: true, describe: 'Check name or sys_id' }),
142
+ handler: wrap(async (argv, app) => {
143
+ const suiteRecord = await resolveScanRecord(app.sdk, baseTable, argv.identifier);
144
+ const check = await resolveScanRecord(app.sdk, 'scan_check', argv.check);
145
+ await guardScope(app, suiteRecord);
146
+ const data = { suite: unwrapSysId(suiteRecord), check: unwrapSysId(check) };
147
+ const table = 'scan_check_suite_check';
148
+ const members = await scanList(app.sdk, table, { query: `suite=${data.suite}^check=${data.check}`, limit: 2 });
149
+ if (members.length > 1) throw errUsage('Duplicate suite memberships exist; inspect and reconcile them before changing membership.');
150
+ let changed = false;
151
+ if (operation === 'add-check' && !members.length) {
152
+ const member = await createRecord(app.sdk, { table, data });
153
+ await verify(app, table, unwrapSysId(member), data);
154
+ changed = true;
155
+ } else if (operation === 'remove-check' && members.length) {
156
+ await confirmDelete(app, argv, 'Remove this check from the suite? The check definition will be retained.');
157
+ await deleteRecord(app.sdk, { table, record: members[0] });
158
+ changed = true;
159
+ }
160
+ report(app, argv, { ...data, changed, member: operation === 'add-check' }, { summary: 'Suite membership reconciled', breadcrumbs: itemHints(group, suiteRecord) });
161
+ }),
162
+ });
163
+ return y;
164
+ },
165
+ handler: wrap(async (argv, app) => report(app, argv, { commands: ['list', 'show', 'create', 'update', 'delete', 'activate', 'deactivate', ...(suite ? ['add-check', 'remove-check'] : [])] }, {
166
+ summary: `Scan ${group}`, breadcrumbs: [hint('list', `jsn scan ${group} list`, `Browse ${group}`)],
167
+ })),
168
+ };
169
+ }
170
+
171
+ export function scanCmd(wrap) {
172
+ return {
173
+ command: 'scan [subcommand]', describe: 'Instance Scan checks, suites, explicit targets, progress and findings',
174
+ builder: y => y.command(definitions('checks', wrap)).command(definitions('suites', wrap))
175
+ .command({ command: 'run', describe: 'Start an explicit target scan (confirmation required; no implicit full scan)',
176
+ builder: p => p.option('update-set', { type: 'string', describe: 'Update set name or sys_id; scans current records' })
177
+ .option('scope', { type: 'string', describe: 'Application scope value or sys_id' })
178
+ .option('record', { type: 'string', describe: 'Record sys_id; requires --table' })
179
+ .option('table', { type: 'string', describe: 'Table for --record' })
180
+ .option('full', { type: 'boolean', describe: 'Explicitly allow an instance-wide scan' })
181
+ .option('previous', { type: 'string', describe: 'Rerun a previous execution target; full scans also require --full' })
182
+ .option('suite', { type: 'string', describe: 'Optional active suite name or sys_id' })
183
+ .option('wait', { type: 'boolean', default: false, describe: 'Wait for completion; timeout does not cancel the scan' })
184
+ .option('timeout', { type: 'number', default: 120, describe: 'Maximum wait in seconds (1–3600)' }),
185
+ handler: wrap(async (argv, app) => {
186
+ if (!Number.isFinite(argv.timeout) || argv.timeout < 1 || argv.timeout > 3600) throw errUsage('--timeout must be from 1 to 3600 seconds.');
187
+ const plan = await prepareScan(app.sdk, argv);
188
+ await confirmDelete(app, argv, `Start scan of ${plan.target.kind} ${plan.target.name || plan.target.sys_id || plan.target.execution_id || 'the full instance'}${plan.suite_id ? ` using suite ${plan.suite_id}` : ''}?`);
189
+ const scheduled = await startScan(app.sdk, plan);
190
+ let data = { ...scheduled, status: 'scheduled', terminal: false };
191
+ if (argv.wait) {
192
+ try {
193
+ const deadline = Date.now() + argv.timeout * 1000;
194
+ while (Date.now() < deadline) {
195
+ try {
196
+ data = { ...scheduled, ...await scanStatus(app.sdk, scheduled.execution_id, { allowInitializing: true }) };
197
+ } catch (error) {
198
+ if (error.code !== 'not_found' && error.status !== 404) throw error;
199
+ // The processor can return before its execution tracker is visible.
200
+ data = { ...scheduled, status: 'scheduled', terminal: false };
201
+ }
202
+ if (data.terminal) { data = { ...scheduled, ...await scanResults(app.sdk, scheduled.execution_id) }; break; }
203
+ await new Promise(resolve => setTimeout(resolve, Math.min(2000, Math.max(0, deadline - Date.now()))));
204
+ }
205
+ if (!data.terminal) data.wait_timed_out = true;
206
+ } catch (error) {
207
+ throw errAPI(error.status || 0, `Scan ${scheduled.execution_id} was scheduled, but progress could not be read: ${error.message}. Use jsn scan status ${scheduled.execution_id}; do not resubmit automatically.`);
208
+ }
209
+ }
210
+ const breadcrumbs = executionHints(data);
211
+ if (data.terminal && !data.reusable_target) breadcrumbs.push(hint('rerun', 'jsn scan run --help', 'Choose a target for another scan'));
212
+ report(app, argv, data, { summary: `Scan ${data.execution_id}: ${data.status}${data.wait_timed_out ? ' (still running; wait timed out)' : ''}`, breadcrumbs });
213
+ }) })
214
+ .command({ command: 'status <id>', describe: 'Read scan execution progress',
215
+ handler: wrap(async (argv, app) => {
216
+ const data = await scanStatus(app.sdk, argv.id);
217
+ report(app, argv, data, { summary: `Scan ${data.status}: ${data.message}`, breadcrumbs: executionHints(data) });
218
+ }) })
219
+ .command({ command: 'results <id>', describe: 'Read findings and check execution coverage; unreadable data is not zero',
220
+ builder: p => p.option('limit', { type: 'number', default: 50, describe: 'Maximum findings/check rows (1–200); truncation is reported' }),
221
+ handler: wrap(async (argv, app) => {
222
+ const data = await scanResults(app.sdk, argv.id, boundedLimit(argv.limit));
223
+ report(app, argv, data, { summary: `Scan ${data.status}; ${data.findings_count ?? 'unknown'} findings; ${data.checks_executed ?? 'unknown'} recorded check executions${data.incomplete ? '; incomplete data' : ''}`, breadcrumbs: executionHints(data) });
224
+ }) })
225
+ .command({ command: 'cancel <id>', describe: 'Request cancellation of this scan after confirmation',
226
+ handler: wrap(async (argv, app) => {
227
+ const status = await scanStatus(app.sdk, argv.id);
228
+ if (!status.terminal) await confirmDelete(app, argv, `Cancel scan ${argv.id}?`);
229
+ const data = status.terminal ? { ...status, cancellation_requested: false } : await cancelScan(app.sdk, argv.id);
230
+ report(app, argv, data, { summary: data.cancellation_requested ? `Cancellation requested: ${argv.id}` : 'Scan already finished; no cancellation sent', breadcrumbs: executionHints(data) });
231
+ }) }),
232
+ handler: wrap(async (argv, app) => report(app, argv, { commands: ['checks', 'suites', 'run', 'status', 'results', 'cancel'] }, { summary: 'Instance Scan', breadcrumbs: rootHints() })),
233
+ };
234
+ }
@@ -3,7 +3,7 @@ import { requireCurrentUserSysId, setCurrentApplication } from '../context.js';
3
3
  import { declareCapabilities } from '../capabilities.js';
4
4
  import { unwrapSysId } from '../resolve-record.js';
5
5
 
6
- declareCapabilities('scopes', { mutationSubcommands: ['create', 'set'] });
6
+ declareCapabilities('scopes', { mutationSubcommands: ['create', 'set'], mutationContextExemptSubcommands: ['create', 'set'] });
7
7
 
8
8
  /**
9
9
  * Resolve a scope identifier to a single sys_scope record.
@@ -1,5 +1,5 @@
1
1
  import { loginWizard, loginProfile, showAuthStatus, switchProfile, modifyProfile, logoutProfile, removeConnection } from './auth.js';
2
- import { errUsage } from '../errors.js';
2
+ import { errUsageHint } from '../errors.js';
3
3
  import { declareCapabilities } from '../capabilities.js';
4
4
 
5
5
  // Setup manages local profile config — runs without a configured instance.
@@ -7,15 +7,36 @@ import { declareCapabilities } from '../capabilities.js';
7
7
  declareCapabilities('setup', { noInstance: true, skipContextHeader: true });
8
8
 
9
9
  const HUB_CHOICES = [
10
- { name: 'Add a connection', value: 'add' },
11
- { name: 'Sign in again', value: 'login' },
12
- { name: 'Check connections', value: 'status' },
13
- { name: 'Choose the default profile', value: 'switch' },
14
- { name: 'Change settings', value: 'modify' },
15
- { name: 'Sign out (keep the profile)', value: 'logout' },
16
- { name: 'Remove a connection', value: 'remove' },
10
+ { name: 'Add a connection', value: 'add', command: 'jsn auth login <instance> --name <name> --basic' },
11
+ { name: 'Sign in again', value: 'login', command: 'jsn auth login --profile <name> (with the authentication options below)' },
12
+ { name: 'Check connections', value: 'status', command: 'jsn auth status --details --json (use --local to skip instance probes)' },
13
+ { name: 'Choose the default profile', value: 'switch', command: 'jsn auth switch <name>' },
14
+ { name: 'Change settings', value: 'modify', command: 'jsn auth modify --profile <name> --read-only --confirm-deletes --no-include-counts' },
15
+ { name: 'Sign out (keep the profile)', value: 'logout', command: 'jsn auth logout --profile <name> --force (only after explicit approval)' },
16
+ { name: 'Remove a connection', value: 'remove', command: 'jsn auth remove <name>' },
17
+ { name: 'Install optional offline documentation', value: 'docs', command: 'jsn docs install --source snapshot|github|skip' },
17
18
  ];
18
19
 
20
+ const setupHelp = () => [
21
+ 'With no profiles, setup opens the add-connection wizard; otherwise it opens the management menu.',
22
+ 'The wizard and these scriptable commands share the same authentication and configuration operations:',
23
+ ...HUB_CHOICES.map(choice => ` ${choice.name}: ${choice.command}`),
24
+ '',
25
+ 'Authentication input without secrets in command arguments:',
26
+ ' Basic: set SN_USERNAME and SN_PASSWORD through your environment, then use --basic.',
27
+ ' OAuth: jsn auth login <instance> --name <name> --oauth --print-url --wait-file <code-file> --json',
28
+ ' Browser session: jsn auth login <instance> --name <name> --headers-file <session-file> --json',
29
+ ' Piped browser session: jsn auth login <instance> --name <name> --headers-file - --json',
30
+ ' A browser window can also be requested explicitly with --browser; it requires a human to sign in.',
31
+ ' Refresh OAuth: jsn auth refresh --profile <name>',
32
+ ' Renew captured browser headers: jsn auth refresh --profile <name> --headers-file <session-file>',
33
+ '',
34
+ 'New-profile preferences: --read-only, --confirm-deletes, --include-counts and --make-default.',
35
+ 'Use --no-read-only, --no-confirm-deletes, --no-include-counts or --no-make-default for the opposite value.',
36
+ 'Use --json for structured output. Explicit flags avoid implicit pickers; ambiguous targets return an error.',
37
+ 'Discover all options with jsn auth <command> --help.',
38
+ ].join('\n');
39
+
19
40
  /** The setup hub prompt; q and Escape resolve with no selected action. */
20
41
  export const authHubPrompt = async (...args) => {
21
42
  const { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } = await import('@inquirer/core');
@@ -81,19 +102,35 @@ export function setupCmd(wrap) {
81
102
  return {
82
103
  command: 'setup',
83
104
  describe: 'Set up and manage your ServiceNow instances (interactive)',
105
+ builder: y => y.option('docs-source', { type: 'string', choices: ['snapshot', 'github', 'skip'], describe: 'Install optional documentation directly without the authentication wizard' }).epilogue(setupHelp()),
84
106
  handler: wrap(async (argv, app) => {
107
+ if (argv['docs-source']) {
108
+ const { installDocumentation } = await import('./docs/install.js');
109
+ app.ok(await installDocumentation(argv['docs-source']));
110
+ return;
111
+ }
85
112
  const { canUsePicker } = await import('../helpers.js');
86
113
  if (!app.isInteractive() || !canUsePicker(app, argv)) {
87
- throw errUsage('"jsn setup" is interactive. Use the specific commands:\n jsn auth login <instance>\n jsn auth refresh\n jsn auth status');
114
+ throw errUsageHint('"jsn setup" is interactive. Use explicit auth commands instead.', setupHelp());
88
115
  }
89
116
  const profileNames = Object.keys(app.config.profiles || {});
90
117
  if (profileNames.length === 0) {
91
118
  const result = await loginWizard(app, argv);
92
119
  await showAuthStatus({ ...argv, profile: result.profileName }, app);
120
+ const { docsDbExists } = await import('./docs/db.js');
121
+ if (!docsDbExists()) {
122
+ const { offerDocumentation } = await import('./docs/install.js');
123
+ app.ok(await offerDocumentation(app, argv));
124
+ }
93
125
  return;
94
126
  }
95
127
  const action = await authHubMenu();
96
128
  if (action === undefined) return;
129
+ if (action === 'docs') {
130
+ const { offerDocumentation } = await import('./docs/install.js');
131
+ app.ok(await offerDocumentation(app, argv));
132
+ return;
133
+ }
97
134
  const result = await dispatchSetupAction(app, argv, action);
98
135
  if (action !== 'status') await showAuthStatus({ ...argv, profile: result?.profile || result?.profileName }, app);
99
136
  }),