@jacebenson/jsn 4.3.13 → 4.4.0-dev.113.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
package/src/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { visibleCommandRegistry } from './command-audience.js';
1
2
  // Root CLI using the local yargs-compatible adapter
2
3
 
3
4
  import yargs, { hideBin } from './cli-adapter.js';
@@ -7,18 +8,21 @@ import fs from 'node:fs';
7
8
  import { loadConfig, globalConfigDir } from './config.js';
8
9
  import { App } from './app.js';
9
10
  import { resolveSession, applySession } from './session.js';
10
- import { renderHelp } from './help.js';
11
+ import { renderHelp, renderTips } from './help.js';
11
12
  import { isMutationCommand, refreshMutationCommands } from './mutations.js';
12
13
  import { skippedUpdatesCmd } from './commands/skipped-updates.js';
13
- import { noInstanceCommands, dailyCheckSkipCommands, contextHeaderSkipCommands } from './capabilities.js';
14
+ import { isLocalPreview, noInstanceCommands, dailyCheckSkipCommands, contextHeaderSkipCommands } from './capabilities.js';
14
15
  import { guardExit, exitWithError, AppError, CodeUsage } from './errors.js';
15
16
  import { formatCommandList } from './helpers.js';
16
17
 
17
18
  // Command modules
19
+ import { discoverCmd } from './commands/discover.js';
18
20
  import { setupCmd } from './commands/setup.js';
19
21
  import { authCmd } from './commands/auth.js';
20
22
  import { recordsCmd } from './commands/records.js';
23
+ import { attachmentsCmd } from './commands/attachments.js';
21
24
  import { cmdbCmd } from './commands/cmdb.js';
25
+ import { knowledgeCmd } from './commands/knowledge.js';
22
26
  import { snippetsCmd } from './commands/snippets.js';
23
27
  import { catalogCmd } from './commands/catalog.js';
24
28
  import { incidentsCmd } from './commands/incidents.js';
@@ -35,8 +39,9 @@ import { transactionsCmd } from './commands/transactions.js';
35
39
  import { platformCmd } from './commands/platform.js';
36
40
  import { codesearchCmd } from './commands/codesearch.js';
37
41
  import { versionCmd } from './commands/version.js';
42
+ import { upgradeCmd } from './commands/upgrade.js';
38
43
  import { skillCmd, checkSkill } from './commands/skill.js';
39
- import { getVersion, checkLatest } from './commands/version.js';
44
+ import { getVersion, checkLatest, getReleaseChannel, getInstallCommand } from './commands/version.js';
40
45
  import { docsCmd } from './commands/docs/docs.js';
41
46
 
42
47
  // Table and platform commands registered at root level for progressive disclosure
@@ -69,7 +74,9 @@ import { logsCmd } from './commands/logs.js';
69
74
  import { scrapiCmd } from './commands/scrapi.js';
70
75
  import { b4rulesCmd } from './commands/b4rules.js';
71
76
  import { atfCmd } from './commands/atf.js';
77
+ import { scanCmd } from './commands/scan.js';
72
78
  import { approvalsCmd } from './commands/approvals.js';
79
+ import { historyCmd } from './commands/history.js';
73
80
  import { perfCmd } from './commands/perf.js';
74
81
  import { customizationsCmd } from './commands/customizations.js';
75
82
  import { midserversCmd } from './commands/midservers.js';
@@ -116,6 +123,7 @@ function wrap(handler) {
116
123
  process.stderr.write('Error: App context not initialized.\n');
117
124
  process.exit(1);
118
125
  }
126
+ await app.guardMutationContext(argv);
119
127
  await handler(argv, app);
120
128
  } catch (err) {
121
129
  // All error rendering goes through the unified renderer in
@@ -144,6 +152,13 @@ export function buildCLI() {
144
152
  },
145
153
  ])
146
154
  .options({
155
+ 'all-commands': { type: 'boolean', describe: 'Show the full command list, including admin and developer tools' },
156
+ reason: { type: 'string', describe: 'Optional non-sensitive reason saved in local mutation history' },
157
+
158
+ force: {
159
+ type: 'boolean',
160
+ description: 'Approve Global + Default mutation context only after user approval; delete commands also use this for their separate confirmation',
161
+ },
147
162
  instance: {
148
163
  type: 'string',
149
164
  description: 'ServiceNow instance URL',
@@ -213,6 +228,7 @@ export function buildCLI() {
213
228
  hint: 'Run "jsn auth status" to list profiles.',
214
229
  });
215
230
  }
231
+ app._authArgv = argv;
216
232
  applySession(app, session);
217
233
  } catch (err) {
218
234
  guardExit(argv, { code: err.code || 'usage', message: err.message, hint: err.hint });
@@ -248,6 +264,7 @@ export function buildCLI() {
248
264
  .middleware(async (argv) => {
249
265
  // Session (incl. --instance/--profile overrides) was already applied
250
266
  // by the middleware above — nothing to re-apply here.
267
+ if (isLocalPreview(argv)) return;
251
268
  const cmd = (argv._[0] || '').toString();
252
269
  // Daily-check + context-header skip-list is derived from command
253
270
  // capability declarations (skipDailyChecks: true).
@@ -290,7 +307,7 @@ export function buildCLI() {
290
307
  }
291
308
  }
292
309
  if (!skipNpmCheck) {
293
- const cacheFile = path.join(globalConfigDir(), '.last-npm-check');
310
+ const cacheFile = path.join(globalConfigDir(), `.last-npm-check-${getReleaseChannel()}`);
294
311
  let shouldCheck = true;
295
312
  try {
296
313
  const mtime = fs.statSync(cacheFile).mtimeMs;
@@ -303,9 +320,9 @@ export function buildCLI() {
303
320
 
304
321
  if (shouldCheck) {
305
322
  const version = getVersion();
306
- checkLatest({ background: true }).then(latest => {
323
+ checkLatest({ background: true, version }).then(latest => {
307
324
  if (latest && version !== latest) {
308
- process.stderr.write(`\n⚠ jsn ${latest} available (you have ${version}) — run "npm install -g @jacebenson/jsn" to update\n\n`);
325
+ process.stderr.write(`\n⚠ jsn ${latest} available (you have ${version}) — run "${getInstallCommand(version)}" to update\n\n`);
309
326
  }
310
327
  }).catch(() => {});
311
328
 
@@ -397,7 +414,9 @@ export function buildCLI() {
397
414
  .command(propertiesCmd(wrap))
398
415
  // DATA
399
416
  .command(cmdbCmd(wrap))
417
+ .command(knowledgeCmd(wrap))
400
418
  .command(recordsCmd(wrap))
419
+ .command(attachmentsCmd(wrap))
401
420
  .command(transactionsCmd(wrap))
402
421
 
403
422
  .command(platformCmd(wrap))
@@ -406,9 +425,11 @@ export function buildCLI() {
406
425
  .command(codesearchCmd(wrap))
407
426
  // DEVELOPER
408
427
  .command(atfCmd(wrap))
428
+ .command(scanCmd(wrap))
409
429
  .command(approvalsCmd(wrap))
410
430
  .command(skippedUpdatesCmd(wrap))
411
431
  .command(perfCmd(wrap))
432
+ .command(historyCmd(wrap))
412
433
  .command(customizationsCmd(wrap))
413
434
  .command(midserversCmd(wrap))
414
435
  .command(diffCmd(wrap))
@@ -418,6 +439,8 @@ export function buildCLI() {
418
439
  .command(skillCmd(wrap))
419
440
  .command(docsCmd(wrap))
420
441
  .command(versionCmd(wrap))
442
+ .command(upgradeCmd(wrap))
443
+ .command(discoverCmd(wrap, (argv) => visibleCommandRegistry(cliInstance.commandRegistry(), app.config, argv)))
421
444
  .demandCommand(1, 'You must specify a command')
422
445
  // Completion plumbing under a hidden command name, so the explicit
423
446
  // 'completion' command below owns the UX. (#176)
@@ -437,6 +460,12 @@ export function buildCLI() {
437
460
  if (current && !current.startsWith('-') && depth === 0) {
438
461
  out = [...new Set([...out, ...rootCommands])];
439
462
  }
463
+ const options = cliInstance.parseGlobalOptions(process.argv.slice(2));
464
+ const visible = visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(options), options);
465
+ if (!depth) {
466
+ const names = new Set(visible.commands.flatMap(command => [command.name, ...command.aliases]));
467
+ out = out.filter(value => value.startsWith('-') || names.has(value));
468
+ }
440
469
  done([...new Set(out)]);
441
470
  });
442
471
  })
@@ -473,7 +502,7 @@ export function buildCLI() {
473
502
  })
474
503
  .onParentDiscovery((argv, { path, subcommands }) => {
475
504
  // Reached only for a parent command that declares no default handler of
476
- // its own (records attachments, docs community, and any future parent) —
505
+ // its own (attachments, docs community, and any future parent) —
477
506
  // the adapter used to fall through and exit 0 having printed nothing.
478
507
  // Children come from the registry, description included, so this list
479
508
  // cannot drift from the registrations the way a hand-written one did.
@@ -498,6 +527,7 @@ export function buildCLI() {
498
527
  }, { summary });
499
528
  })(argv);
500
529
  })
530
+ .epilogue(renderTips())
501
531
  .help('help', 'Show help')
502
532
  .version(false)
503
533
  .strictCommands()
@@ -541,7 +571,7 @@ export function buildCLI() {
541
571
  if (typeof formatArgv?.get === 'string' && formatArgv.get) {
542
572
  app.output.setJqFilter(formatArgv.get);
543
573
  }
544
- const { commands } = cliInstance.commandRegistry();
574
+ const { commands } = visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(formatArgv), formatArgv);
545
575
  try {
546
576
  app.ok({ commands }, {
547
577
  summary: `${commands.length} registered commands — run "jsn <command> --json" for a command's subcommands.`,
@@ -551,7 +581,7 @@ export function buildCLI() {
551
581
  }
552
582
  process.exit(0);
553
583
  }
554
- process.stdout.write(renderHelp(loadConfig(), cliInstance.commandRegistry()) + '\n');
584
+ process.stdout.write(renderHelp(loadConfig(formatArgv), visibleCommandRegistry(cliInstance.commandRegistry(), loadConfig(formatArgv), formatArgv)) + '\n');
555
585
  process.exit(0);
556
586
  }
557
587
  // Every other failure here (unknown command, missing required argument,
@@ -571,6 +601,12 @@ export function buildCLI() {
571
601
  exitWithError(new AppError(CodeUsage, msg, hint), { app });
572
602
  });
573
603
 
604
+ cliInstance.rootHelpRenderer = () => {
605
+ const options = cliInstance.parseGlobalOptions(process.argv.slice(2));
606
+ const config = loadConfig(options);
607
+ return renderHelp(config, visibleCommandRegistry(cliInstance.commandRegistry(), config, options));
608
+ };
609
+
574
610
  // Snapshot the root command names (plus aliases) for the completion
575
611
  // filter. Done before any parse — command builders mutate the registry.
576
612
  rootCommands = cliInstance.getInternalMethods().getCommandInstance().getCommands();
@@ -0,0 +1,38 @@
1
+ // Presentation policy only. ServiceNow ACLs still authorize every request.
2
+ // Explicit command paths and --all-commands remain available to every account.
3
+ const BASIC_COMMANDS = new Set([
4
+ 'incidents', 'changes', 'requests', 'tasks', 'tickets', 'approvals', 'records',
5
+ 'knowledge',
6
+ 'groups', 'groupmembers', 'grouproles', 'users', 'setup', 'auth', 'skill',
7
+ 'cmdb',
8
+ 'version', 'upgrade', 'docs', 'history', 'discover', 'completion',
9
+ 'catalogitems',
10
+ ]);
11
+
12
+ export async function detectCommandAudience(sdk, username, instanceURL = sdk?.baseURL) {
13
+ if (!username || typeof sdk?.listStrict !== 'function') return undefined;
14
+ try {
15
+ const roles = await sdk.listStrict('sys_user_has_role', {
16
+ sysparm_query: 'user=javascript:gs.getUserID()^role.name=admin',
17
+ sysparm_fields: 'sys_id', sysparm_limit: '1',
18
+ }, { touchLastSeen: false });
19
+ return { username, instance_url: instanceURL, mode: roles.length ? 'admin' : 'basic', role_check: 'verified' };
20
+ } catch {
21
+ // Missing role access must not expand help. This is a presentation fallback,
22
+ // not a claim that the role query succeeded or an authorization boundary.
23
+ return { username, instance_url: instanceURL, mode: 'basic', role_check: 'unavailable' };
24
+ }
25
+ }
26
+
27
+ export function commandAudience(config, { instance, allCommands = false } = {}) {
28
+ if (allCommands || instance) return 'all';
29
+ const name = config?.activeProfile || config?.defaultProfile;
30
+ const profile = config?.profiles?.[name];
31
+ const cached = profile?.command_audience;
32
+ return cached?.username === profile?.username && cached?.instance_url === profile?.instance_url && cached?.mode === 'basic' ? 'basic' : 'all';
33
+ }
34
+
35
+ export function visibleCommandRegistry(registry, config, options = {}) {
36
+ if (commandAudience(config, options) !== 'basic') return registry;
37
+ return { ...registry, commands: registry.commands.filter(command => BASIC_COMMANDS.has(command.name)) };
38
+ }
@@ -1,7 +1,10 @@
1
+ import { constrainQuery, constrainedQueryHelp } from '../constrained-query.js';
2
+ import { enrichRelatedLists, relatedListOptions } from '../related-lists.js';
1
3
  // Generic table-based CRUD command builder
2
4
 
3
5
  import { formatRecordForDisplay, getStringField, buildRecordPayload, confirmDelete, interactiveList, canPrompt } from '../helpers.js';
4
- import { resolveRecord, unwrapSysId } from '../resolve-record.js';
6
+ import { getRecord, listRecords, recordListParams, createRecord, updateRecord, deleteRecord, recordOptions } from '../records-engine.js';
7
+ import { unwrapSysId } from '../resolve-record.js';
5
8
  import { getCurrentUser, getCurrentApplication } from '../context.js';
6
9
  import { FormatAuto } from '../output.js';
7
10
  import { declareCapabilities } from '../capabilities.js';
@@ -120,19 +123,24 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
120
123
  command: 'list',
121
124
  aliases: ['ls'],
122
125
  describe: `List ${name}`,
123
- builder: (y) => y
124
- .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
125
- .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
126
- .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
126
+ builder: (y) => {
127
+ const command = recordOptions(y)
128
+ .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
129
+ .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
130
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' });
131
+ const help = [opts.help, extraQuery ? constrainedQueryHelp(extraQuery) : ''].filter(Boolean).join('\n');
132
+ return help ? command.epilogue(help) : command;
133
+ },
127
134
  handler: wrap(async (argv, app) => {
128
135
  const query = argv.query || '';
136
+ const constrainedQuery = constrainQuery(query, extraQuery);
129
137
  const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
130
138
  const limit = argv.limit;
131
139
 
132
140
  // Interactive picker with pagination
133
141
  if (canPrompt() && app.output.getFormat() === FormatAuto && !query) {
134
142
  const picked = await interactiveList({
135
- app, table, singular, columns: ['name', 'sys_scope'], limit, query: extraQuery,
143
+ app, table, singular, columns: ['name', 'sys_scope'], limit, query: constrainedQuery,
136
144
  labelField: 'name',
137
145
  message: `Select ${vowelArticle(singular)} ${singular}`,
138
146
  formatLabel: (r) => {
@@ -147,7 +155,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
147
155
  const recordName = getStringField(record, 'name') || getStringField(record, 'sys_id');
148
156
  record._context = { instance_url: app.getEffectiveInstance(), table };
149
157
  if (opts.onShow) {
150
- await opts.onShow(record, app);
158
+ await opts.onShow(record, app, argv);
151
159
  }
152
160
  app.ok(record, {
153
161
  summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${recordName}`,
@@ -160,19 +168,14 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
160
168
  }
161
169
 
162
170
  // Non-interactive: text/table output
163
- const params = new URLSearchParams();
164
- params.set('sysparm_limit', String(limit));
165
- params.set('sysparm_display_value', 'all');
166
- params.set('sysparm_fields', ['sys_id', ...columns].join(','));
167
- const baseQ = extraQuery ? extraQuery + '^' : '';
168
- const q = query ? query + '^ORDERBYDESCsys_updated_on' : (baseQ + 'ORDERBYDESCsys_updated_on');
169
- params.set('sysparm_query', q);
170
- const records = await app.sdk.list(table, params);
171
+ const q = `${constrainedQuery}${constrainedQuery ? '^' : ''}ORDERBYDESCsys_updated_on`;
172
+ const params = recordListParams({ limit, fields: ['sys_id', ...columns], query: q });
173
+ const records = await listRecords(app.sdk, { table, params, displayValues: argv.displayValues ?? opts.referenceMode });
171
174
  app.ok({
172
175
  table,
173
176
  count: records.length,
174
177
  columns,
175
- records: records.map(r => formatRecordForDisplay(r, columns)),
178
+ records: argv.displayValues || opts.listProjection === 'full' ? records : records.map(r => formatRecordForDisplay(r, columns)),
176
179
  context: { instance_url: app.getEffectiveInstance() },
177
180
  }, {
178
181
  summary: `${records.length} ${name}(s)`,
@@ -184,6 +187,11 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
184
187
  command: opts.resolveIdentifier ? 'show [identifier]' : 'show <identifier>',
185
188
  aliases: ['get'],
186
189
  describe: `Show ${vowelArticle(singular)} ${singular} by name or sys_id`,
190
+ builder: y => {
191
+ const command = relatedListOptions(recordOptions(y));
192
+ return opts.relationships?.essential?.length ? command.option('essential', { type: 'boolean', default: true,
193
+ describe: 'Fetch profile-declared essential children; --no-essential skips them. Supplementary lists remain lazy.' }) : command;
194
+ },
187
195
  handler: wrap(async (argv, app) => {
188
196
  const id = opts.resolveIdentifier ? await opts.resolveIdentifier(argv, app) : argv.identifier;
189
197
  if (id === undefined) return;
@@ -192,16 +200,17 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
192
200
  const fields = (showFields && showFields.length > 0)
193
201
  ? [...new Set(['sys_id', ...showFields])]
194
202
  : undefined;
195
- const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular, fields });
203
+ const record = await getRecord(app.sdk, { table, identifier: id, identifierField: argv.identifierField, displayValues: argv.displayValues ?? opts.referenceMode, matchField: opts.identifierField || 'name', resource: singular, fields });
196
204
  record._context = {
197
205
  instance_url: app.getEffectiveInstance(),
198
206
  table,
199
207
  };
200
208
 
201
209
  if (opts.onShow) {
202
- await opts.onShow(record, app);
210
+ await opts.onShow(record, app, argv);
203
211
  }
204
212
 
213
+ await enrichRelatedLists(record, { table, argv, app });
205
214
  const summary = typeof showSummary === 'function' ? showSummary(record, id) : showSummary;
206
215
  const breadcrumbs = typeof showBreadcrumbs === 'function' ? showBreadcrumbs(record, id) : showBreadcrumbs;
207
216
 
@@ -214,14 +223,14 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
214
223
  .command({
215
224
  command: 'create',
216
225
  describe: `Create a new ${singular}`,
217
- builder: (y) => y
226
+ builder: (y) => recordOptions(y, false)
218
227
  .option('data', { type: 'string', describe: 'JSON fields' })
219
228
  .option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
220
229
  .option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
221
230
  .option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
222
231
  handler: wrap(async (argv, app) => {
223
232
  const recordData = buildRecordPayload(argv);
224
- const record = await app.sdk.create(table, recordData);
233
+ const record = await createRecord(app.sdk, { table, data: recordData });
225
234
  app.ok(record, {
226
235
  summary: `Created ${singular}`,
227
236
  breadcrumbs: [
@@ -233,31 +242,31 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
233
242
  .command({
234
243
  command: 'update <identifier>',
235
244
  describe: `Update ${vowelArticle(singular)} ${singular}`,
236
- builder: (y) => y
245
+ builder: (y) => recordOptions(y, false)
237
246
  .option('data', { type: 'string', describe: 'JSON fields' })
238
247
  .option('data-file', { type: 'string', describe: 'Read JSON payload from file' })
239
248
  .option('data-stdin', { type: 'boolean', describe: 'Read JSON payload from stdin' })
240
249
  .option('field', { alias: 'f', type: 'string', array: true, describe: 'Set a field as name=value; repeatable' }),
241
250
  handler: wrap(async (argv, app) => {
242
251
  const id = argv.identifier;
243
- const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular });
252
+ const record = await getRecord(app.sdk, { table, identifier: id, identifierField: argv.identifierField, displayValues: argv.displayValues ?? opts.referenceMode, matchField: opts.identifierField || 'name', resource: singular });
244
253
  if (scopeValidation) {
245
254
  await guardScope(app.sdk, record);
246
255
  }
247
256
 
248
257
  const sysID = unwrapSysId(record);
249
258
  const recordData = buildRecordPayload(argv);
250
- const updated = await app.sdk.update(table, sysID, recordData);
259
+ const updated = await updateRecord(app.sdk, { table, record: { sys_id: sysID }, data: recordData });
251
260
  app.ok(updated, { summary: `Updated ${singular} ${id}` });
252
261
  }),
253
262
  })
254
263
  .command({
255
264
  command: 'delete <identifier>',
256
265
  describe: `Delete ${vowelArticle(singular)} ${singular}`,
257
- builder: (y) => y.option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
266
+ builder: (y) => recordOptions(y, false).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
258
267
  handler: wrap(async (argv, app) => {
259
268
  const id = argv.identifier;
260
- const record = await resolveRecord(app.sdk, { table, identifier: id, matchField: 'name', resource: singular });
269
+ const record = await getRecord(app.sdk, { table, identifier: id, identifierField: argv.identifierField, displayValues: argv.displayValues ?? opts.referenceMode, matchField: opts.identifierField || 'name', resource: singular });
261
270
  if (scopeValidation) {
262
271
  await guardScope(app.sdk, record);
263
272
  }
@@ -267,19 +276,27 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
267
276
 
268
277
  await confirmDelete(app, argv, `Delete ${singular} '${recordName}'`);
269
278
 
270
- await app.sdk.delete(table, sysID);
279
+ await deleteRecord(app.sdk, { table, record: { sys_id: sysID } });
271
280
  app.ok({ name: recordName, sys_id: sysID, deleted: true }, { summary: `Deleted ${singular} '${recordName}'` });
272
281
  }),
273
282
  });
274
283
  }
275
284
 
276
- return y;
285
+ const help = [opts.help, extraQuery ? constrainedQueryHelp(extraQuery) : ''].filter(Boolean).join('\n');
286
+ return help ? y.epilogue(help) : y;
277
287
  };
278
288
 
279
289
  return {
280
290
  command: `${name} [subcommand]`,
281
291
  aliases: aliases || [],
282
- describe: `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
292
+ describe: opts.describe || `Manage ${name} (e.g. "${name} list --query nameLIKEincident")`,
293
+ discovery: {
294
+ table,
295
+ defaultColumns,
296
+ benefits: ['Configured list columns', 'Lookup by name or sys_id',
297
+ ...(showFields ? ['Configured show fields'] : []),
298
+ ...(opts.onShow ? ['Additional table-specific show processing'] : [])],
299
+ },
283
300
  builder,
284
301
  handler: (_argv) => {
285
302
  // This handler only runs when no subcommand is matched (the default case).
@@ -1,4 +1,4 @@
1
- import { buildDevCmd } from './_generic.js';
1
+ import { buildProfileCommand } from '../record-profiles.js';
2
2
  import { getStringField } from '../helpers.js';
3
3
  // Simple table-based commands — singular names passed for grammar
4
4
  // Default columns and aliases match the Go version exactly
@@ -47,13 +47,13 @@ async function fetchExtensionChain(sdk, record) {
47
47
 
48
48
  export { actionsCmd } from './actions.js';
49
49
 
50
- export const includesCmd = (wrap) => buildDevCmd('includes', 'sys_script_include', ['include', 'si'], ['name', 'api_name', 'active', 'sys_scope'], wrap, { singular: 'script include', scopeValidation: true });
50
+ export const includesCmd = (wrap) => buildProfileCommand('includes', wrap);
51
51
 
52
- export const rulesCmd = (wrap) => buildDevCmd('rules', 'sys_script', ['rule', 'br'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'business rule', scopeValidation: true });
52
+ export const rulesCmd = (wrap) => buildProfileCommand('rules', wrap);
53
53
 
54
- export const clientScriptsCmd = (wrap) => buildDevCmd('clientscripts', 'sys_script_client', ['clientscript', 'cs'], ['name', 'table', 'active', 'type', 'sys_scope'], wrap, { singular: 'client script', scopeValidation: true });
54
+ export const clientScriptsCmd = (wrap) => buildProfileCommand('clientscripts', wrap);
55
55
 
56
- export const uiActionsCmd = (wrap) => buildDevCmd('uiactions', 'sys_ui_action', ['uiaction', 'ua'], ['name', 'table', 'active', 'order', 'sys_scope'], wrap, { singular: 'UI action', scopeValidation: true });
56
+ export const uiActionsCmd = (wrap) => buildProfileCommand('uiactions', wrap);
57
57
 
58
58
  // NOTE: uipolicies has exactly ONE definition — the rich one in
59
59
  // ./uipolicies.js (re-exported below at the bottom of this file). A plain
@@ -62,10 +62,7 @@ export const uiActionsCmd = (wrap) => buildDevCmd('uiactions', 'sys_ui_action',
62
62
  // this module's uiPoliciesCmd before uipoliciesCmd — so the factory version
63
63
  // was dead code shadowing nothing. Deleted; the re-export is the behavior.
64
64
 
65
- export const tablesCmd = (wrap) => buildDevCmd('tables', 'sys_db_object', ['table', 't'], ['name', 'label', 'super_class', 'create_access_controls'], wrap, {
66
- singular: 'table',
67
- scopeValidation: true,
68
- showFields: ['name', 'label', 'super_class', 'create_access_controls', 'sys_scope', 'sys_created_on', 'sys_updated_on', 'sys_created_by', 'sys_updated_by', 'is_extendable'],
65
+ export const tablesCmd = (wrap) => buildProfileCommand('tables', wrap, {
69
66
  async onShow(record, app) {
70
67
  const tableName = getStringField(record, 'name');
71
68
  const [count, extChain] = await Promise.all([
@@ -81,48 +78,48 @@ export { columnsCmd } from './columns.js';
81
78
 
82
79
  // Read-only commands (Go only has list/show)
83
80
  export { importCmd } from './import.js';
84
- export const spPagesCmd = (wrap) => buildDevCmd('sppages', 'sp_page', ['sp-pages'], ['id', 'title', 'sys_scope'], wrap, { singular: 'Service Portal page', readOnly: true });
85
- export const spWidgetsCmd = (wrap) => buildDevCmd('spwidgets', 'sp_widget', ['sp-widget', 'widgets'], ['id', 'name', 'sys_scope'], wrap, { singular: 'Service Portal widget', readOnly: true });
86
- export const uiPagesCmd = (wrap) => buildDevCmd('uipages', 'sys_ui_page', ['ui-page', 'pages'], ['name', 'sys_scope'], wrap, { singular: 'UI page', scopeValidation: true });
87
- export const appMenuCmd = (wrap) => buildDevCmd('appmenu', 'sys_app_application', ['app-menu', 'menu'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application menu', scopeValidation: true });
81
+ export const spPagesCmd = (wrap) => buildProfileCommand('sppages', wrap);
82
+ export const spWidgetsCmd = (wrap) => buildProfileCommand('spwidgets', wrap);
83
+ export const uiPagesCmd = (wrap) => buildProfileCommand('uipages', wrap);
84
+ export const appMenuCmd = (wrap) => buildProfileCommand('appmenu', wrap);
88
85
 
89
86
  // Commands with full CRUD
90
- export const aclsCmd = (wrap) => buildDevCmd('acls', 'sys_security_acl', ['acl'], ['name', 'operation', 'type', 'active', 'sys_scope'], wrap, { singular: 'ACL', scopeValidation: true });
91
- export const rolesCmd = (wrap) => buildDevCmd('roles', 'sys_user_role', ['role', 'r'], ['name', 'description', 'elevated_privilege', 'sys_scope'], wrap, { singular: 'role', scopeValidation: true });
92
- export const propertiesCmd = (wrap) => buildDevCmd('properties', 'sys_properties', ['property', 'prop'], ['name', 'value', 'description', 'sys_scope'], wrap, { singular: 'property', scopeValidation: true });
87
+ export const aclsCmd = (wrap) => buildProfileCommand('acls', wrap);
88
+ export const rolesCmd = (wrap) => buildProfileCommand('roles', wrap);
89
+ export const propertiesCmd = (wrap) => buildProfileCommand('properties', wrap);
93
90
 
94
91
  // New commands for scoped-app tables
95
- export const relationshipsCmd = (wrap) => buildDevCmd('relationships', 'sys_relationship', ['relationship', 'rel'], ['name', 'sys_scope'], wrap, { singular: 'relationship', scopeValidation: true });
96
- export const appmodulesCmd = (wrap) => buildDevCmd('appmodules', 'sys_app_module', ['appmodule', 'am'], ['name', 'active', 'sys_scope'], wrap, { singular: 'application module', scopeValidation: true });
97
- export const listcontrolsCmd = (wrap) => buildDevCmd('listcontrols', 'sys_ui_list_control', ['listcontrol', 'lc'], ['name', 'active', 'sys_scope'], wrap, { singular: 'list control', scopeValidation: true });
98
- export const viewsCmd = (wrap) => buildDevCmd('views', 'sys_ui_view', ['view', 'vw'], ['name', 'title', 'sys_scope'], wrap, { singular: 'view', scopeValidation: true });
99
- export const privilegesCmd = (wrap) => buildDevCmd('privileges', 'sys_scope_privilege', ['privilege', 'priv'], ['source_scope', 'target_scope', 'target_name', 'operation', 'status'], wrap, { singular: 'privilege', scopeValidation: true });
100
- export const uxscriptsCmd = (wrap) => buildDevCmd('uxscripts', 'sys_ux_lib_source_script', ['uxscript', 'ux'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UX script', scopeValidation: true });
92
+ export const relationshipsCmd = (wrap) => buildProfileCommand('relationships', wrap);
93
+ export const appmodulesCmd = (wrap) => buildProfileCommand('appmodules', wrap);
94
+ export const listcontrolsCmd = (wrap) => buildProfileCommand('listcontrols', wrap);
95
+ export const viewsCmd = (wrap) => buildProfileCommand('views', wrap);
96
+ export const privilegesCmd = (wrap) => buildProfileCommand('privileges', wrap);
97
+ export const uxscriptsCmd = (wrap) => buildProfileCommand('uxscripts', wrap);
101
98
  export { aliasesCmd } from './aliases.js';
102
- export const catalogscriptsCmd = (wrap) => buildDevCmd('catalogscripts', 'catalog_script_client', ['catalogscript'], ['name', 'table', 'active', 'sys_scope'], wrap, { singular: 'catalog script', scopeValidation: true });
99
+ export const catalogscriptsCmd = (wrap) => buildProfileCommand('catalogscripts', wrap);
103
100
 
104
101
  // ── Automation: Async ──
105
- export const scriptactionsCmd = (wrap) => buildDevCmd('scriptactions', 'sysevent_script_action', ['scriptaction'], ['name', 'active', 'sys_scope'], wrap, { singular: 'script action', scopeValidation: true });
106
- export const scheduledjobsCmd = (wrap) => buildDevCmd('scheduledjobs', 'sysauto_script', ['scheduledjob'], ['name', 'active', 'run_type', 'sys_scope'], wrap, { singular: 'scheduled job', scopeValidation: true });
107
- export const asyncrulesCmd = (wrap) => buildDevCmd('asyncrules', 'sys_script', ['asyncrule'], ['name', 'collection', 'active', 'order', 'sys_scope'], wrap, { singular: 'async business rule', scopeValidation: true, extraQuery: 'when=async' });
108
- export const triggersCmd = (wrap) => buildDevCmd('triggers', 'sysevent_register', ['trigger'], ['name', 'event_name', 'active', 'sys_scope'], wrap, { singular: 'event trigger', scopeValidation: true });
102
+ export const scriptactionsCmd = (wrap) => buildProfileCommand('scriptactions', wrap);
103
+ export const scheduledjobsCmd = (wrap) => buildProfileCommand('scheduledjobs', wrap);
104
+ export const asyncrulesCmd = (wrap) => buildProfileCommand('asyncrules', wrap);
105
+ export const triggersCmd = (wrap) => buildProfileCommand('triggers', wrap);
109
106
 
110
107
  // ── Automation: In Memory ──
111
108
 
112
- export const assignmentsCmd = (wrap) => buildDevCmd('assignments', 'sysrule_assignment', ['assignment'], ['name', 'active', 'sys_scope'], wrap, { singular: 'assignment rule', scopeValidation: true, readOnly: true });
109
+ export const assignmentsCmd = (wrap) => buildProfileCommand('assignments', wrap);
113
110
 
114
111
  // ── Automation: Inbound ──
115
- export const emailCmd = (wrap) => buildDevCmd('email', 'sysevent_in_email_action', ['emails'], ['name', 'active', 'type', 'sys_scope'], wrap, { singular: 'inbound email action', scopeValidation: true });
112
+ export const emailCmd = (wrap) => buildProfileCommand('email', wrap);
116
113
 
117
114
  // ── Automation: Outbound ──
118
115
  export { restmessageCmd } from './restmessage.js';
119
116
  export { soapmessagesCmd } from './soapmessages.js';
120
117
 
121
118
  // ── UX: Core UI ──
122
- export const uimacrosCmd = (wrap) => buildDevCmd('uimacros', 'sys_ui_macro', ['uimacro'], ['name', 'active', 'sys_scope'], wrap, { singular: 'UI macro', scopeValidation: true });
119
+ export const uimacrosCmd = (wrap) => buildProfileCommand('uimacros', wrap);
123
120
 
124
121
  // ── UX: Workspaces ──
125
- export const uxlistsCmd = (wrap) => buildDevCmd('uxlists', 'sys_ux_list', ['uxlist'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace list', scopeValidation: true, readOnly: true });
126
- export const uxapplicabilityCmd = (wrap) => buildDevCmd('uxapplicability', 'sys_ux_applicability_m2m', ['uxapp'], ['name', 'active', 'sys_scope'], wrap, { singular: 'workspace applicability', scopeValidation: true, readOnly: true });
122
+ export const uxlistsCmd = (wrap) => buildProfileCommand('uxlists', wrap);
123
+ export const uxapplicabilityCmd = (wrap) => buildProfileCommand('uxapplicability', wrap);
127
124
  export { uipoliciesCmd } from './uipolicies.js';
128
- export const cataloguipoliciesCmd = (wrap) => buildDevCmd('cataloguipolicies', 'catalog_ui_policy', ['cataloguipolicy', 'cup'], ['short_description', 'catalog_item', 'variable_set', 'active', 'sys_scope'], wrap, { singular: 'catalog UI policy', scopeValidation: true });
125
+ export const cataloguipoliciesCmd = (wrap) => buildProfileCommand('cataloguipolicies', wrap);