@jacebenson/jsn 3.0.6 → 3.1.0

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.
@@ -18,7 +18,7 @@ compatibility: |
18
18
  the correct command path.
19
19
  metadata:
20
20
  author: jacebenson
21
- version: "1.0.0"
21
+ version: "1.1.0"
22
22
  repository: https://github.com/jacebenson/jsn
23
23
  ---
24
24
 
@@ -31,9 +31,8 @@ Explore and manage ServiceNow instances. Works standalone or with AI agents.
31
31
  Commands are self-documenting. Use these to learn what's available:
32
32
 
33
33
  ```bash
34
- jsn --help # Root help with all top-level commands
34
+ jsn --help # Root help with all top-level commands (grouped by category)
35
35
  jsn <command> --help # Detailed usage, flags, and examples for any command
36
- jsn dev # Shows categorized dev subcommands
37
36
  ```
38
37
 
39
38
  ## Agent Rules
@@ -60,7 +59,7 @@ jsn dev # Shows categorized dev subcommands
60
59
  - `jsn records --table <name> create`
61
60
  - `jsn records --table <name> update <sys_id>`
62
61
  - `jsn records --table <name> delete <sys_id>`
63
- - `jsn dev updatesets set <name>`
62
+ - `jsn updatesets set <name>`
64
63
  - `jsn profiles remove <name>`
65
64
  - Any `delete`, `update`, `create`, `set`, or `remove` subcommand
66
65
 
@@ -121,6 +120,44 @@ All commands support `--json`. The envelope structure:
121
120
 
122
121
  Breadcrumbs suggest next commands — follow them for navigation.
123
122
 
123
+ ## Troubleshooting Field Behavior
124
+
125
+ When a user asks about field behavior (why a picker won't show inactive users, why a field is read-only, why a reference qualifier behaves unexpectedly), do NOT just check dictionary records or ACLs. Field behavior is often controlled by **before-query business rules**.
126
+
127
+ **Check order (priority):**
128
+
129
+ 1. **Before-query business rules** — `sys_script` with `when=before`, `action=query`, matching `collection` (table)
130
+ ```bash
131
+ jsn records list --table sys_script --query "collection=sys_user^when=before^action=query^active=true" --json
132
+ ```
133
+ A common case: inactive user filters on `sys_user` are implemented as a before-query BR that adds `active=true` to the query condition.
134
+
135
+ 2. **ACLs** — `sys_security_acl` — read/write/delete permissions on tables and fields
136
+ ```bash
137
+ jsn acls list --query "name=incident" --json
138
+ ```
139
+
140
+ 3. **Dictionary overrides** — `sys_dictionary` — reference qualifiers, field attributes, default values
141
+ ```bash
142
+ jsn records list --table sys_dictionary --query "name=incident^element=caller_id" --json
143
+ ```
144
+
145
+ 4. **Client scripts** — `sys_script_client` — UI-side visibility, read-only, or value-change logic
146
+ ```bash
147
+ jsn records list --table sys_script_client --query "table=incident^active=true" --json
148
+ ```
149
+
150
+ 5. **UI policies** — `sys_ui_policy` — form-level field behavior (mandatory, read-only, visible)
151
+ ```bash
152
+ jsn records list --table sys_ui_policy --query "table=incident^active=true" --json
153
+ ```
154
+
155
+ **Example:** "Why can't I pick an inactive user on the incident form?"
156
+ - First guess: Dictionary reference qualifier on `caller_id`? → Check `sys_dictionary`
157
+ - Most likely: Before-query BR on `sys_user` that filters `active=true` → Check `sys_script` first
158
+
159
+ **Rule of thumb:** If a picker is silently filtering results (no error, just fewer options), it's a before-query BR. If it shows an error, it's an ACL or dictionary constraint.
160
+
124
161
  ## Configuration
125
162
 
126
163
  ```
package/src/app.js CHANGED
@@ -118,7 +118,7 @@ export class App {
118
118
 
119
119
  const scopeFormatted = `[${scope}]`;
120
120
 
121
- process.stderr.write('# Use `jsn dev scopes` to change scope, `jsn dev updatesets set` to change updateset\n');
121
+ process.stderr.write('# Use `jsn scopes` to change scope, `jsn updatesets set` to change updateset\n');
122
122
  process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
123
123
 
124
124
  const profileStr = `]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}]8;;\x07`;
@@ -144,12 +144,12 @@ export class App {
144
144
  '┃ manual surgery, one change at a time. Avoid the mess. ┃\n' +
145
145
  '┃ ┃\n' +
146
146
  '┃ Create a named update set now: ┃\n' +
147
- '┃ jsn dev updatesets create --name "My Feature" ┃\n' +
147
+ '┃ jsn updatesets create --name "My Feature" ┃\n' +
148
148
  '┃ ┃\n' +
149
149
  '┃ Or switch to a scoped scope first: ┃\n' +
150
- '┃ jsn dev scopes list ┃\n' +
150
+ '┃ jsn scopes list ┃\n' +
151
151
  '┃ ┃\n' +
152
- '┃ (Run jsn dev updatesets yolo to silence this check) ┃\n' +
152
+ '┃ (Run jsn updatesets yolo to silence this check) ┃\n' +
153
153
  '┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\x1b[0m\n'
154
154
  );
155
155
  } else {
@@ -160,7 +160,7 @@ export class App {
160
160
  ' ⚠ Default update set in scope [' + scope + ']\n' +
161
161
  ' Changes are contained to this scope, but a named\n' +
162
162
  ' update set is still recommended for tracking.\n' +
163
- ' (Run \x1b[1mjsn dev updatesets yolo\x1b[22m to silence this warning)\n' +
163
+ ' (Run \x1b[1mjsn updatesets yolo\x1b[22m to silence this warning)\n' +
164
164
  '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
165
165
  '\x1b[0m' // reset
166
166
  );
package/src/cli.js CHANGED
@@ -27,6 +27,27 @@ import { versionCmd } from './commands/version.js';
27
27
  import { devCmd } from './commands/dev.js';
28
28
  import { skillCmd, checkSkill } from './commands/skill.js';
29
29
 
30
+ // Dev subcommands promoted to root level for progressive disclosure
31
+ import {
32
+ actionsCmd, includesCmd, rulesCmd,
33
+ clientScriptsCmd, uiActionsCmd, uiPoliciesCmd,
34
+ tablesCmd, columnsCmd, importCmd,
35
+ spPagesCmd, spWidgetsCmd, uiPagesCmd, appMenuCmd,
36
+ aclsCmd, rolesCmd, propertiesCmd,
37
+ relationshipsCmd, appmodulesCmd, listcontrolsCmd, viewsCmd,
38
+ privilegesCmd, securitytypesCmd, uxscriptsCmd, aliasesCmd,
39
+ catalogscriptsCmd, cataloguipoliciesCmd,
40
+ } from './commands/dev/_simple.js';
41
+ import { flowsCmd } from './commands/dev/flows.js';
42
+ import { formsCmd } from './commands/dev/forms.js';
43
+ import { listsCmd } from './commands/dev/lists.js';
44
+ import { updateSetsCmd } from './commands/dev/updatesets.js';
45
+ import { scopesCmd } from './commands/dev/scopes.js';
46
+ import { evalCmd } from './commands/dev/eval.js';
47
+ import { restCmd } from './commands/dev/rest.js';
48
+ import { logsCmd } from './commands/dev/logs.js';
49
+ import { scrapiCmd } from './commands/dev/scrapi.js';
50
+
30
51
  function wrap(handler) {
31
52
  return async (argv) => {
32
53
  try {
@@ -177,6 +198,45 @@ export const cli = yargs(hideBin(process.argv))
177
198
  .command(groupRolesCmd(wrap))
178
199
  .command(ticketsCmd(wrap))
179
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))
180
240
  .command(skillCmd(wrap))
181
241
  .command(versionCmd(wrap))
182
242
  .demandCommand(1, 'You must specify a command')
@@ -2,7 +2,7 @@
2
2
  // Used by incidents, changes, requests, tasks, and most dev subcommands
3
3
 
4
4
  import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam } from '../helpers.js';
5
- import search from '@inquirer/search';
5
+ import { search } from '@inquirer/prompts';
6
6
  import { isTTY, FormatAuto } from '../output.js';
7
7
 
8
8
  export function buildTicketCommands(table, displayName, alias, defaultColumns, stateMap, iconFn, wrap) {
@@ -3,7 +3,7 @@
3
3
  import { formatRecordForDisplay, getStringField, isHexString, parseDataArg } from '../../helpers.js';
4
4
  import { getCurrentUser, getCurrentApplication } from '../../context.js';
5
5
  import readline from 'node:readline';
6
- import search from '@inquirer/search';
6
+ import { search } from '@inquirer/prompts';
7
7
  import { isTTY, FormatAuto } from '../../output.js';
8
8
 
9
9
  function vowelArticle(word) {
@@ -16,13 +16,13 @@ function buildHints(name, singular, readOnly) {
16
16
  if (!readOnly) {
17
17
  crumbs.push({
18
18
  action: 'create',
19
- cmd: `jsn dev ${name} create --name ... --label "..."`,
19
+ cmd: `jsn ${name} create --name ... --label "..."`,
20
20
  description: `Create ${vowelArticle(singular)} ${singular}`,
21
21
  });
22
22
  }
23
23
  crumbs.push({
24
24
  action: 'show',
25
- cmd: `jsn dev ${name} show <name_or_sys_id>`,
25
+ cmd: `jsn ${name} show <name_or_sys_id>`,
26
26
  description: `Show ${singular} details`,
27
27
  });
28
28
  return crumbs;
@@ -84,12 +84,12 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
84
84
  const showSummary = opts.showSummary || ((record, id) => `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${getStringField(record, 'name') || id}`);
85
85
  const showBreadcrumbs = opts.showBreadcrumbs || ((record, id) => {
86
86
  const crumbs = [
87
- { action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
87
+ { action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
88
88
  ];
89
89
  if (!readOnly) {
90
90
  crumbs.unshift(
91
- { action: 'delete', cmd: `jsn dev ${name} delete ${id}`, description: `Delete this ${singular}` },
92
- { action: 'update', cmd: `jsn dev ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
91
+ { action: 'delete', cmd: `jsn ${name} delete ${id}`, description: `Delete this ${singular}` },
92
+ { action: 'update', cmd: `jsn ${name} update ${id} --data '{...}'`, description: `Update this ${singular}` }
93
93
  );
94
94
  }
95
95
  return crumbs;
@@ -104,7 +104,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
104
104
  builder: (y) => y
105
105
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true^priority=1")' })
106
106
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "name,label,super_class")' })
107
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
107
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
108
108
  handler: wrap(async (argv, app) => {
109
109
  const query = argv.query || '';
110
110
  const columns = argv.columns ? argv.columns.split(',') : defaultColumns;
@@ -178,8 +178,8 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
178
178
  app.ok(record, {
179
179
  summary: `${singular.charAt(0).toUpperCase() + singular.slice(1)}: ${selectedName}`,
180
180
  breadcrumbs: [
181
- ...(readOnly ? [] : [{ action: 'update', cmd: `jsn dev ${name} update ${selectedName} --data '{...}'`, description: `Update this ${singular}` }]),
182
- { action: 'list', cmd: `jsn dev ${name} list`, description: `Back to all ${name}` },
181
+ ...(readOnly ? [] : [{ action: 'update', cmd: `jsn ${name} update ${selectedName} --data '{...}'`, description: `Update this ${singular}` }]),
182
+ { action: 'list', cmd: `jsn ${name} list`, description: `Back to all ${name}` },
183
183
  ],
184
184
  });
185
185
  }
@@ -256,7 +256,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
256
256
  app.ok(record, {
257
257
  summary: `Created ${singular}`,
258
258
  breadcrumbs: [
259
- { action: 'show', cmd: `jsn dev ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
259
+ { action: 'show', cmd: `jsn ${name} show ${getStringField(record, 'name') || getStringField(record, 'sys_id')}`, description: `View the new ${singular}` },
260
260
  ],
261
261
  });
262
262
  }),
@@ -284,7 +284,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
284
284
  if (scopeValidation) {
285
285
  const scopeErr = await checkScope(app.sdk, recordScope);
286
286
  if (scopeErr) {
287
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
287
+ throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
288
288
  }
289
289
  }
290
290
 
@@ -315,7 +315,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
315
315
  if (scopeValidation) {
316
316
  const scopeErr = await checkScope(app.sdk, recordScope);
317
317
  if (scopeErr) {
318
- throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn dev scopes set ${scopeErr.recordScope}`);
318
+ throw new Error(`record is in scope '${scopeErr.recordScope}', but your current scope is '${scopeErr.currentScope}'. Switch scope first: jsn scopes set ${scopeErr.recordScope}`);
319
319
  }
320
320
  }
321
321
 
@@ -358,7 +358,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
358
358
  console.log(' update Update a record');
359
359
  console.log(' delete Delete a record');
360
360
  }
361
- console.log(`\nRun "jsn dev ${name} <command> --help" for details.`);
361
+ console.log(`\nRun "jsn ${name} <command> --help" for details.`);
362
362
  },
363
363
  };
364
364
  }
@@ -45,7 +45,7 @@ export function evalCmd(wrap) {
45
45
  summary: 'Script executed' + warning,
46
46
  breadcrumbs: [{
47
47
  action: 'eval',
48
- cmd: 'jsn dev eval --script \'...\'',
48
+ cmd: 'jsn eval --script \'...\'',
49
49
  description: 'Execute a background script on the ServiceNow instance',
50
50
  }],
51
51
  });
@@ -14,22 +14,24 @@ export function flowsCmd(wrap) {
14
14
  builder: (y) => y
15
15
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
16
16
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
17
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
17
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
18
  handler: wrap(async (argv, app) => {
19
+ app.requireInstance();
19
20
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'active', 'description', 'sys_created_by', 'sys_updated_on'];
20
21
  const query = argv.query || '';
21
22
 
22
23
  // Interactive picker
23
24
  const picked = await interactiveList({
24
25
  app, table: 'sys_hub_flow', singular: 'flow', columns, limit: argv.limit, query, labelField: 'name',
25
- formatLabel: r => `${getStringField(r, 'name')} ${getStringField(r, 'active') === 'true' ? '' : '[inactive]'}`,
26
+ formatLabel: r => `${getStringField(r, 'active') === 'true' ? '🟢' : '🔴'} ${getStringField(r, 'name')}`,
26
27
  });
28
+ if (picked === undefined) return; // user cancelled
27
29
  if (picked) {
28
30
  const inspection = await app.sdk.inspectFlow(getStringField(picked, 'sys_id'));
29
31
  const formatted = formatFlowInspection(inspection, app.getEffectiveInstance());
30
32
  return app.ok({ ...inspection, _formatted: formatted }, {
31
33
  summary: `Flow: ${inspection.flow.name}`,
32
- breadcrumbs: [{ action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' }],
34
+ breadcrumbs: [{ action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' }],
33
35
  });
34
36
  }
35
37
 
@@ -61,7 +63,7 @@ export function flowsCmd(wrap) {
61
63
  app.ok(data, {
62
64
  summary: `Flow: ${inspection.flow.name}`,
63
65
  breadcrumbs: [
64
- { action: 'list', cmd: 'jsn dev flows list', description: 'Back to all flows' },
66
+ { action: 'list', cmd: 'jsn flows list', description: 'Back to all flows' },
65
67
  ],
66
68
  });
67
69
  }),
@@ -73,7 +75,7 @@ export function flowsCmd(wrap) {
73
75
  .option('data', { type: 'string', describe: 'JSON data for the flow' }),
74
76
  handler: wrap(async (_argv, _app) => {
75
77
  throw new Error('Flow creation requires the Flow Designer GraphQL API - not yet implemented.\n'
76
- + 'Use the ServiceNow web UI to create flows, then use "jsn dev flows list" to view them.');
78
+ + 'Use the ServiceNow web UI to create flows, then use "jsn flows list" to view them.');
77
79
  }),
78
80
  })
79
81
  .command({
@@ -83,7 +85,7 @@ export function flowsCmd(wrap) {
83
85
  .option('data', { type: 'string', describe: 'JSON data to update' }),
84
86
  handler: wrap(async (_argv, _app) => {
85
87
  throw new Error('Flow updates require the Flow Designer GraphQL API - not yet implemented.\n'
86
- + 'Use the ServiceNow web UI to update flows, then use "jsn dev flows list" to view them.');
88
+ + 'Use the ServiceNow web UI to update flows, then use "jsn flows list" to view them.');
87
89
  }),
88
90
  })
89
91
  .command({
@@ -95,7 +97,22 @@ export function flowsCmd(wrap) {
95
97
  }),
96
98
  });
97
99
  },
98
- handler: () => {},
100
+ handler: () => {
101
+ console.log('Manage Flow Designer flows from the sys_hub_flow table.');
102
+ console.log('');
103
+ console.log('Available subcommands:');
104
+ console.log(' list List flows');
105
+ console.log(' show <identifier> Show flow details by name or sys_id');
106
+ console.log(' create Create a new flow (not yet implemented)');
107
+ console.log(' update <identifier> Update a flow (not yet implemented)');
108
+ console.log(' delete <identifier> Delete a flow (not yet implemented)');
109
+ console.log('');
110
+ console.log('Run "jsn flows <command> --help" for details.');
111
+ console.log('');
112
+ console.log('Note: Flow inspection detail varies. V2 flows (with payload)');
113
+ console.log('show full action inputs and conditions. Subflows and V1 flows');
114
+ console.log('show step names only.');
115
+ },
99
116
  };
100
117
  }
101
118
 
@@ -168,7 +185,13 @@ function formatFlowInspection(inspection, instanceURL) {
168
185
  lines.push('');
169
186
  lines.push('⚡ FLOW STRUCTURE');
170
187
  lines.push('─'.repeat(50));
171
- lines.push(...formatFlowStructure(inspection));
188
+ const structureLines = formatFlowStructure(inspection);
189
+ lines.push(...structureLines);
190
+ // Note limitation for V1/subflows without payload detail
191
+ if (!inspection.payload || Object.keys(inspection.payload).length === 0) {
192
+ lines.push('');
193
+ lines.push(' (step detail limited — this flow lacks a V2 payload)');
194
+ }
172
195
 
173
196
  lines.push('');
174
197
  return lines.join('\n') + '\n';
@@ -60,6 +60,7 @@ export function formsCmd(wrap) {
60
60
  })
61
61
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
62
62
  handler: wrap(async (argv, app) => {
63
+ app.requireInstance();
63
64
  const table = argv.table;
64
65
  const limit = argv.limit || 50;
65
66
 
@@ -99,7 +100,7 @@ export function formsCmd(wrap) {
99
100
  }, {
100
101
  summary: `${views.length} views for ${table}`,
101
102
  breadcrumbs: [
102
- { action: 'show', cmd: `jsn dev forms show ${table} --view "Default view"`, description: 'Show Default view layout' },
103
+ { action: 'show', cmd: `jsn forms show ${table} --view "Default view"`, description: 'Show Default view layout' },
103
104
  ],
104
105
  });
105
106
  }),
@@ -111,6 +112,7 @@ export function formsCmd(wrap) {
111
112
  builder: (y) => y
112
113
  .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
113
114
  handler: wrap(async (argv, app) => {
115
+ app.requireInstance();
114
116
  const table = argv.table;
115
117
  const viewName = argv.view;
116
118
 
@@ -256,8 +258,8 @@ export function formsCmd(wrap) {
256
258
  lines.push('─────');
257
259
  lines.push('');
258
260
  lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
259
- lines.push(` ${`jsn dev forms list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
260
- lines.push(` ${`jsn dev columns --table ${table}`.padEnd(50)} ${chalk.hex('#888888')('View table columns')}`);
261
+ lines.push(` ${`jsn forms list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
262
+ lines.push(` ${`jsn columns --table ${table}`.padEnd(50)} ${chalk.hex('#888888')('View table columns')}`);
261
263
  lines.push('');
262
264
 
263
265
  const formatted = lines.join('\n');
@@ -305,13 +307,21 @@ export function formsCmd(wrap) {
305
307
  }, {
306
308
  summary: `Form: ${table} (${viewName}) - ${sections.length} sections`,
307
309
  breadcrumbs: [
308
- { action: 'list', cmd: `jsn dev forms list ${table}`, description: 'List all views' },
309
- { action: 'columns', cmd: `jsn dev columns --table ${table}`, description: 'View table columns' },
310
+ { action: 'list', cmd: `jsn forms list ${table}`, description: 'List all views' },
311
+ { action: 'columns', cmd: `jsn columns --table ${table}`, description: 'View table columns' },
310
312
  ],
311
313
  });
312
314
  }),
313
315
  });
314
316
  },
315
- handler: () => {},
317
+ handler: () => {
318
+ console.log('Manage form layouts from the sys_ui_form_section table.');
319
+ console.log('');
320
+ console.log('Available subcommands:');
321
+ console.log(' list <table> List form views for a table');
322
+ console.log(' show <table> Show form layout for a table');
323
+ console.log('');
324
+ console.log('Run "jsn forms <command> --help" for details.');
325
+ },
316
326
  };
317
327
  }
@@ -51,6 +51,7 @@ export function listsCmd(wrap) {
51
51
  })
52
52
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
53
53
  handler: wrap(async (argv, app) => {
54
+ app.requireInstance();
54
55
  const table = argv.table;
55
56
  const limit = argv.limit || 50;
56
57
 
@@ -90,7 +91,7 @@ export function listsCmd(wrap) {
90
91
  }, {
91
92
  summary: `${views.length} list views for ${table}`,
92
93
  breadcrumbs: [
93
- { action: 'show', cmd: `jsn dev lists show ${table} --view "Default view"`, description: 'Show Default view columns' },
94
+ { action: 'show', cmd: `jsn lists show ${table} --view "Default view"`, description: 'Show Default view columns' },
94
95
  ],
95
96
  });
96
97
  }),
@@ -102,6 +103,7 @@ export function listsCmd(wrap) {
102
103
  builder: (y) => y
103
104
  .option('view', { type: 'string', default: 'Default view', describe: 'View name' }),
104
105
  handler: wrap(async (argv, app) => {
106
+ app.requireInstance();
105
107
  const table = argv.table;
106
108
  const viewName = argv.view;
107
109
 
@@ -195,8 +197,8 @@ export function listsCmd(wrap) {
195
197
  lines.push('─────');
196
198
  lines.push('');
197
199
  lines.push(chalk.bold(chalk.hex('#e8a217')('Hints:')));
198
- lines.push(` ${`jsn dev lists list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
199
- lines.push(` ${`jsn dev forms show ${table} --view "${viewName}"`.padEnd(50)} ${chalk.hex('#888888')('Show form layout')}`);
200
+ lines.push(` ${`jsn lists list ${table}`.padEnd(50)} ${chalk.hex('#888888')('List all views')}`);
201
+ lines.push(` ${`jsn forms show ${table} --view "${viewName}"`.padEnd(50)} ${chalk.hex('#888888')('Show form layout')}`);
200
202
  lines.push('');
201
203
 
202
204
  const formatted = lines.join('\n');
@@ -225,13 +227,21 @@ export function listsCmd(wrap) {
225
227
  }, {
226
228
  summary: `List: ${table} (${viewName}) - ${elements.length} columns`,
227
229
  breadcrumbs: [
228
- { action: 'list', cmd: `jsn dev lists list ${table}`, description: 'List all views' },
229
- { action: 'form', cmd: `jsn dev forms show ${table} --view "${viewName}"`, description: 'Show form layout' },
230
+ { action: 'list', cmd: `jsn lists list ${table}`, description: 'List all views' },
231
+ { action: 'form', cmd: `jsn forms show ${table} --view "${viewName}"`, description: 'Show form layout' },
230
232
  ],
231
233
  });
232
234
  }),
233
235
  });
234
236
  },
235
- handler: () => {},
237
+ handler: () => {
238
+ console.log('Manage list layouts from the sys_ui_list table.');
239
+ console.log('');
240
+ console.log('Available subcommands:');
241
+ console.log(' list <table> List list views for a table');
242
+ console.log(' show <table> Show list layout for a table');
243
+ console.log('');
244
+ console.log('Run "jsn lists <command> --help" for details.');
245
+ },
236
246
  };
237
247
  }
@@ -16,6 +16,7 @@ export function logsCmd(wrap) {
16
16
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
17
17
  .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
18
  handler: wrap(async (argv, app) => {
19
+ app.requireInstance();
19
20
  const columns = argv.columns ? argv.columns.split(',') : ['level', 'message', 'source', 'created'];
20
21
  const query = argv.query || '';
21
22
 
@@ -24,6 +25,7 @@ export function logsCmd(wrap) {
24
25
  app, table: 'syslog', singular: 'log entry', columns, limit: argv.limit, query, labelField: 'message',
25
26
  formatLabel: r => `[${getStringField(r, 'level') || '?'}] ${(getStringField(r, 'message') || '').substring(0, 80)}`,
26
27
  });
28
+ if (picked === undefined) return; // user cancelled
27
29
  if (picked) {
28
30
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'syslog' };
29
31
  return app.ok(picked, { summary: `Log entry` });
@@ -59,6 +61,14 @@ export function logsCmd(wrap) {
59
61
  }),
60
62
  });
61
63
  },
62
- handler: () => {},
64
+ handler: () => {
65
+ console.log('View system logs from the syslog table.');
66
+ console.log('');
67
+ console.log('Available subcommands:');
68
+ console.log(' list List log entries');
69
+ console.log(' show <sys_id> Show a log entry by sys_id');
70
+ console.log('');
71
+ console.log('Run "jsn logs <command> --help" for details.');
72
+ },
63
73
  };
64
74
  }
@@ -14,7 +14,7 @@ export function scopesCmd(wrap) {
14
14
  builder: (y) => y
15
15
  .option('query', { type: 'string', describe: 'Encoded query (e.g. "nameLIKEincident" or "active=true")' })
16
16
  .option('columns', { alias: ['c', 'fields'], type: 'string', describe: 'Comma-separated columns (e.g. "number,short_description")' })
17
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' }),
17
+ .option('limit', { alias: 'l', type: 'number', default: 50, describe: 'Max records' }),
18
18
  handler: wrap(async (argv, app) => {
19
19
  const columns = argv.columns ? argv.columns.split(',') : ['name', 'scope', 'short_description', 'active'];
20
20
  const query = argv.query || '';
@@ -23,6 +23,7 @@ export function scopesCmd(wrap) {
23
23
  app, table: 'sys_scope', singular: 'scope', columns, limit: argv.limit, query, labelField: 'name',
24
24
  formatLabel: r => `${getStringField(r, 'name')} [${getStringField(r, 'scope') || '?'}]`,
25
25
  });
26
+ if (picked === undefined) return; // user cancelled
26
27
  if (picked) {
27
28
  picked._context = { instance_url: app.getEffectiveInstance(), table: 'sys_scope' };
28
29
  return app.ok(picked, { summary: `Scope: ${getStringField(picked, 'name')}` });
@@ -143,7 +144,7 @@ export function scopesCmd(wrap) {
143
144
  summary: `Created scope: ${scope}`,
144
145
  breadcrumbs: [{
145
146
  action: 'show',
146
- cmd: `jsn dev scopes show ${scope}`,
147
+ cmd: `jsn scopes show ${scope}`,
147
148
  description: 'View the new scope',
148
149
  }],
149
150
  });
@@ -159,7 +160,7 @@ export function scopesCmd(wrap) {
159
160
  console.log(' show <scope> Show a scope');
160
161
  console.log(' set <scope> Set the current application scope');
161
162
  console.log(' create Create a new application scope');
162
- console.log('\nRun "jsn dev scopes <command> --help" for details.');
163
+ console.log('\nRun "jsn scopes <command> --help" for details.');
163
164
  }
164
165
  },
165
166
  };