@jacebenson/jsn 4.0.3 → 4.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.
- package/package.json +1 -3
- package/src/app.js +25 -3
- package/src/cli.js +17 -1
- package/src/commands/_ticket.js +3 -3
- package/src/commands/auth.js +116 -7
- package/src/commands/catalog.js +66 -4
- package/src/commands/dev/_generic.js +3 -3
- package/src/commands/dev/domains.js +292 -0
- package/src/commands/dev/eval.js +27 -5
- package/src/commands/dev/scopes.js +88 -36
- package/src/commands/dev/updatesets.js +297 -106
- package/src/commands/dev.js +16 -1
- package/src/commands/records.js +23 -5
- package/src/context.js +52 -5
- package/src/errors.js +15 -0
- package/src/help.js +16 -2
- package/src/helpers.js +110 -5
- package/src/mutations.js +10 -1
- package/src/output.js +8 -3
- package/src/sdk.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacebenson/jsn",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,8 +29,6 @@
|
|
|
29
29
|
"@inquirer/core": "^10.1.0",
|
|
30
30
|
"@inquirer/prompts": "^7.5.1",
|
|
31
31
|
"better-sqlite3": "^13.0.3",
|
|
32
|
-
"chalk": "^5.4.1",
|
|
33
|
-
"cli-table3": "^0.6.5",
|
|
34
32
|
"gray-matter": "^4.0.3",
|
|
35
33
|
"turndown": "^7.2.4",
|
|
36
34
|
"yargs": "^17.7.2"
|
package/src/app.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { AuthManager } from './auth.js';
|
|
4
4
|
import { SDKClient } from './sdk.js';
|
|
5
5
|
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
|
|
6
|
-
import { getEffectiveInstance, normalizeInstanceURL } from './config.js';
|
|
6
|
+
import { getEffectiveInstance, normalizeInstanceURL, saveConfig } from './config.js';
|
|
7
7
|
import { extractProfileName } from './helpers.js';
|
|
8
8
|
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
9
9
|
import { errUsage, errAuth } from './errors.js';
|
|
@@ -20,7 +20,9 @@ export class App {
|
|
|
20
20
|
|
|
21
21
|
const instance = getEffectiveInstance(cfg);
|
|
22
22
|
if (instance) {
|
|
23
|
-
this.sdk = new SDKClient(instance, this.auth
|
|
23
|
+
this.sdk = new SDKClient(instance, this.auth, {
|
|
24
|
+
domain: this._profileDomain(cfg),
|
|
25
|
+
});
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
this.context = {
|
|
@@ -202,9 +204,29 @@ export class App {
|
|
|
202
204
|
const normalizedUrl = normalizeInstanceURL(url);
|
|
203
205
|
if (normalizedUrl) {
|
|
204
206
|
this._overrideInstance = normalizedUrl;
|
|
205
|
-
this.sdk = new SDKClient(normalizedUrl, this.auth
|
|
207
|
+
this.sdk = new SDKClient(normalizedUrl, this.auth, {
|
|
208
|
+
domain: this._profileDomain(this.config),
|
|
209
|
+
});
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
|
|
213
|
+
/** Read the configured domain for the active profile (empty = no scoping). */
|
|
214
|
+
_profileDomain(cfg) {
|
|
215
|
+
const name = cfg.activeProfile || cfg.defaultProfile;
|
|
216
|
+
if (name && cfg.profiles[name]) {
|
|
217
|
+
return cfg.profiles[name].domain || '';
|
|
218
|
+
}
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Set (or clear with '') the domain for the active profile. */
|
|
223
|
+
setDomain(domain) {
|
|
224
|
+
const name = this.config.activeProfile || this.config.defaultProfile;
|
|
225
|
+
if (!name) throw errUsage('No active profile to set a domain on');
|
|
226
|
+
if (!this.config.profiles[name]) this.config.profiles[name] = {};
|
|
227
|
+
this.config.profiles[name].domain = domain || '';
|
|
228
|
+
return saveConfig(this.config);
|
|
229
|
+
}
|
|
208
230
|
}
|
|
209
231
|
|
|
210
232
|
function resolveFormat(fmt) {
|
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { App } from './app.js';
|
|
|
10
10
|
import { renderHelp } from './help.js';
|
|
11
11
|
import { isMutationCommand } from './mutations.js';
|
|
12
12
|
import { guardExit } from './errors.js';
|
|
13
|
+
import { FormatJSON } from './output.js';
|
|
13
14
|
|
|
14
15
|
// Command modules
|
|
15
16
|
import { setupCmd } from './commands/setup.js';
|
|
@@ -53,6 +54,7 @@ import { formsCmd } from './commands/dev/forms.js';
|
|
|
53
54
|
import { listsCmd } from './commands/dev/lists.js';
|
|
54
55
|
import { updateSetsCmd } from './commands/dev/updatesets.js';
|
|
55
56
|
import { scopesCmd } from './commands/dev/scopes.js';
|
|
57
|
+
import { domainsCmd } from './commands/dev/domains.js';
|
|
56
58
|
import { evalCmd } from './commands/dev/eval.js';
|
|
57
59
|
import { restCmd } from './commands/dev/rest.js';
|
|
58
60
|
import { logsCmd } from './commands/dev/logs.js';
|
|
@@ -85,6 +87,19 @@ function wrap(handler) {
|
|
|
85
87
|
process.stderr.write(`Error (system): ${err.message}\n`);
|
|
86
88
|
process.exit(3);
|
|
87
89
|
}
|
|
90
|
+
if (err.code === 'confirmation_required') {
|
|
91
|
+
// Structured error for AI agents / piped consumers: emit the
|
|
92
|
+
// JSON envelope on stdout (where tooling reads) so the agent can
|
|
93
|
+
// surface the decision to its user or re-run with --force.
|
|
94
|
+
const app = argv.app;
|
|
95
|
+
if (app && app.output.effectiveFormat() === FormatJSON) {
|
|
96
|
+
app.err(err);
|
|
97
|
+
} else {
|
|
98
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
99
|
+
if (err.hint) process.stderr.write(`\n${err.hint}\n`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
88
103
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
89
104
|
process.exit(1);
|
|
90
105
|
}
|
|
@@ -287,6 +302,7 @@ export function buildCLI() {
|
|
|
287
302
|
.command(authCmd(wrap))
|
|
288
303
|
.command(updateSetsCmd(wrap))
|
|
289
304
|
.command(scopesCmd(wrap))
|
|
305
|
+
.command(domainsCmd(wrap))
|
|
290
306
|
// CORE
|
|
291
307
|
.command(incidentsCmd(wrap))
|
|
292
308
|
.command(changesCmd(wrap))
|
|
@@ -388,7 +404,7 @@ export function buildCLI() {
|
|
|
388
404
|
}
|
|
389
405
|
process.exit(0);
|
|
390
406
|
}
|
|
391
|
-
process.stdout.write(renderHelp() + '\n');
|
|
407
|
+
process.stdout.write(renderHelp(loadConfig()) + '\n');
|
|
392
408
|
process.exit(0);
|
|
393
409
|
}
|
|
394
410
|
process.stderr.write(msg + '\n');
|
package/src/commands/_ticket.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generic command builder for CRUD operations on a ServiceNow table
|
|
2
2
|
// Used by incidents, changes, requests, tasks, and most dev subcommands
|
|
3
3
|
|
|
4
|
-
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, assertSafeExactMatch } from '../helpers.js';
|
|
4
|
+
import { getStringField, formatRecordForDisplay, buildQuerySuffix, resolveFieldsParam, confirmDelete, assertSafeExactMatch, canPrompt } from '../helpers.js';
|
|
5
5
|
import { search } from '@inquirer/prompts';
|
|
6
|
-
import {
|
|
6
|
+
import { FormatAuto } from '../output.js';
|
|
7
7
|
|
|
8
8
|
export function buildTicketCommands(table, displayName, alias, defaultColumns, stateMap, iconFn, wrap) {
|
|
9
9
|
return {
|
|
@@ -28,7 +28,7 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
|
|
|
28
28
|
const offset = argv.offset;
|
|
29
29
|
|
|
30
30
|
// Interactive picker in TTY with auto format and no explicit query/offset
|
|
31
|
-
const isInteractive =
|
|
31
|
+
const isInteractive = canPrompt();
|
|
32
32
|
if (isInteractive && app.output.getFormat() === FormatAuto && !query && offset === 0) {
|
|
33
33
|
const pickerColumns = ['sys_id', 'number', 'short_description', 'state', 'assigned_to'];
|
|
34
34
|
const pickerParams = new URLSearchParams();
|
package/src/commands/auth.js
CHANGED
|
@@ -151,6 +151,36 @@ export async function loginWizard(app, argv = {}) {
|
|
|
151
151
|
}
|
|
152
152
|
console.log('Login successful!');
|
|
153
153
|
loggedIn = true;
|
|
154
|
+
|
|
155
|
+
// Probe domain separation capability once and stamp the profile, so
|
|
156
|
+
// `jsn domains` (and its help entry) only appear on instances with it.
|
|
157
|
+
try {
|
|
158
|
+
const { SDKClient } = await import('../sdk.js');
|
|
159
|
+
const domainsMod = await import('./dev/domains.js');
|
|
160
|
+
const sdk = new SDKClient(instance, app.auth);
|
|
161
|
+
profile.domain_separation = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
162
|
+
await setProfile(app.config, profileName, profile);
|
|
163
|
+
|
|
164
|
+
// If domain separation is installed, offer to pick a default domain
|
|
165
|
+
// for the profile — every request will be scoped to it via X-Now-Domain.
|
|
166
|
+
if (profile.domain_separation) {
|
|
167
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
168
|
+
const setDomainNow = await confirm({
|
|
169
|
+
message: 'Domain separation detected. Configure a domain for this instance?',
|
|
170
|
+
default: false,
|
|
171
|
+
});
|
|
172
|
+
if (setDomainNow) {
|
|
173
|
+
const domainSysID = await domainsMod.pickDomain({ ...app, sdk });
|
|
174
|
+
if (domainSysID) {
|
|
175
|
+
profile.domain = domainSysID;
|
|
176
|
+
await setProfile(app.config, profileName, profile);
|
|
177
|
+
console.log('Domain configured.');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Non-fatal — capability unknown; domains command falls back to probing
|
|
183
|
+
}
|
|
154
184
|
}
|
|
155
185
|
} else {
|
|
156
186
|
rl.close();
|
|
@@ -210,24 +240,82 @@ const PROFILE_FLAGS = [
|
|
|
210
240
|
];
|
|
211
241
|
|
|
212
242
|
/**
|
|
213
|
-
*
|
|
214
|
-
* Interactive
|
|
243
|
+
* Modify a profile: toggle a boolean flag, or set/clear the domain.
|
|
244
|
+
* Interactive picker when no flag passed; testable via argv.flag/argv.domain.
|
|
215
245
|
*/
|
|
216
246
|
export async function modifyProfile(app, argv = {}) {
|
|
217
247
|
const name = argv.name || await pickProfile(app, 'Modify which instance?');
|
|
218
248
|
const profile = app.config.profiles?.[name];
|
|
219
249
|
if (!profile) throw new Error(`Profile not found: ${name}`);
|
|
220
250
|
|
|
251
|
+
// Domain setting path: --domain <name|sys_id|clear>
|
|
252
|
+
if (argv.domain !== undefined) {
|
|
253
|
+
if (!profile.domain_separation) {
|
|
254
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url} — no domain to configure.`);
|
|
255
|
+
}
|
|
256
|
+
if (argv.domain === 'clear' || argv.domain === 'none' || argv.domain === '') {
|
|
257
|
+
profile.domain = '';
|
|
258
|
+
await saveConfig(app.config);
|
|
259
|
+
app.ok({ profile: name, domain: '' }, { summary: `Domain cleared for ${name}` });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
263
|
+
const { SDKClient } = await import('../sdk.js');
|
|
264
|
+
const sdk = new SDKClient(profile.instance_url, app.auth);
|
|
265
|
+
if (!(await isDomainSeparationInstalled({ sdk }))) {
|
|
266
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
267
|
+
}
|
|
268
|
+
const { resolveDomainSysId } = await import('./dev/domains.js');
|
|
269
|
+
const sysID = await resolveDomainSysId({ sdk }, argv.domain);
|
|
270
|
+
profile.domain = sysID;
|
|
271
|
+
await saveConfig(app.config);
|
|
272
|
+
app.ok({ profile: name, domain: sysID }, { summary: `Domain set for ${name}` });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
221
276
|
let flag = argv.flag;
|
|
222
277
|
if (!flag) {
|
|
223
278
|
const { select } = await import('@inquirer/prompts');
|
|
279
|
+
const choices = PROFILE_FLAGS.map(f => ({
|
|
280
|
+
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
281
|
+
value: f.value,
|
|
282
|
+
}));
|
|
283
|
+
if (profile.domain_separation) {
|
|
284
|
+
choices.push({
|
|
285
|
+
name: `🌐 Domain: ${profile.domain ? 'set' : 'not set'}`,
|
|
286
|
+
value: 'domain',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const { SDKClient } = await import('../sdk.js');
|
|
290
|
+
const sdk = new SDKClient(profile.instance_url, app.auth);
|
|
224
291
|
flag = await select({
|
|
225
292
|
message: `Modify ${name} (${profile.instance_url})`,
|
|
226
|
-
choices
|
|
227
|
-
name: `${f.icon} ${f.name}: ${profile[f.value] ? 'on' : 'off'}`,
|
|
228
|
-
value: f.value,
|
|
229
|
-
})),
|
|
293
|
+
choices,
|
|
230
294
|
});
|
|
295
|
+
if (flag === 'domain') {
|
|
296
|
+
const domainsMod = await import('./dev/domains.js');
|
|
297
|
+
const installed = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
298
|
+
if (!installed) {
|
|
299
|
+
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
300
|
+
}
|
|
301
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
302
|
+
const clear = profile.domain
|
|
303
|
+
? await confirm({ message: `Domain is set. Clear it?`, default: false })
|
|
304
|
+
: false;
|
|
305
|
+
if (clear) {
|
|
306
|
+
profile.domain = '';
|
|
307
|
+
await saveConfig(app.config);
|
|
308
|
+
app.ok({ profile: name, domain: '' }, { summary: `Domain cleared for ${name}` });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const sysID = await domainsMod.pickDomain({ ...app, sdk });
|
|
312
|
+
if (sysID) {
|
|
313
|
+
profile.domain = sysID;
|
|
314
|
+
await saveConfig(app.config);
|
|
315
|
+
app.ok({ profile: name, domain: sysID }, { summary: `Domain set for ${name}` });
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
231
319
|
}
|
|
232
320
|
|
|
233
321
|
profile[flag] = !profile[flag];
|
|
@@ -558,6 +646,23 @@ Examples:
|
|
|
558
646
|
|
|
559
647
|
const creds = await app.auth.getCredentialsFor(instanceURL);
|
|
560
648
|
const refreshed = await app.auth.refreshToken(instanceURL, creds);
|
|
649
|
+
|
|
650
|
+
// Re-probe domain separation on refresh so the capability flag
|
|
651
|
+
// stays in sync if the plugin was installed/removed since setup.
|
|
652
|
+
try {
|
|
653
|
+
const { SDKClient } = await import('../sdk.js');
|
|
654
|
+
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
655
|
+
const sdk = new SDKClient(instanceURL, app.auth);
|
|
656
|
+
const hasDS = await isDomainSeparationInstalled({ sdk });
|
|
657
|
+
const name = app.config.activeProfile || app.config.defaultProfile;
|
|
658
|
+
if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
|
|
659
|
+
app.config.profiles[name].domain_separation = hasDS;
|
|
660
|
+
await saveConfig(app.config);
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
// Non-fatal — capability probe is best-effort
|
|
664
|
+
}
|
|
665
|
+
|
|
561
666
|
app.ok({
|
|
562
667
|
refreshed: true,
|
|
563
668
|
instance: instanceURL,
|
|
@@ -604,11 +709,15 @@ Examples:
|
|
|
604
709
|
})
|
|
605
710
|
.command({
|
|
606
711
|
command: 'modify [name]',
|
|
607
|
-
describe: 'Toggle instance configuration (read-only, skip confirmations)',
|
|
712
|
+
describe: 'Toggle instance configuration (read-only, skip confirmations, domain)',
|
|
608
713
|
builder: (sub) => sub
|
|
609
714
|
.positional('name', {
|
|
610
715
|
describe: 'Profile name to modify',
|
|
611
716
|
type: 'string',
|
|
717
|
+
})
|
|
718
|
+
.option('domain', {
|
|
719
|
+
type: 'string',
|
|
720
|
+
describe: 'Set the domain for this profile (name, sys_id, or "clear")',
|
|
612
721
|
}),
|
|
613
722
|
handler: wrap(async (argv, app) => {
|
|
614
723
|
await modifyProfile(app, argv);
|
package/src/commands/catalog.js
CHANGED
|
@@ -175,7 +175,15 @@ export function catalogCmd(wrap) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
async function showItem(app, sysID) {
|
|
178
|
-
|
|
178
|
+
// Use list + display_value=all so reference fields (category, flow) resolve
|
|
179
|
+
// to readable display values instead of raw sys_ids.
|
|
180
|
+
const p = new URLSearchParams();
|
|
181
|
+
p.set('sysparm_query', `sys_id=${sysID}`);
|
|
182
|
+
p.set('sysparm_limit', '1');
|
|
183
|
+
p.set('sysparm_display_value', 'all');
|
|
184
|
+
const items = await app.sdk.list('sc_cat_item', p);
|
|
185
|
+
const item = items[0] || null;
|
|
186
|
+
if (!item) throw new Error(`Not found: ${sysID}`);
|
|
179
187
|
|
|
180
188
|
const vp = new URLSearchParams();
|
|
181
189
|
vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
|
|
@@ -216,7 +224,8 @@ async function showItem(app, sysID) {
|
|
|
216
224
|
const wfName = getStringField(item, 'workflow') || '';
|
|
217
225
|
const wfID = item.workflow?.value || '';
|
|
218
226
|
const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan') || '';
|
|
219
|
-
|
|
227
|
+
|
|
228
|
+
const data = {
|
|
220
229
|
name: getStringField(item, 'name'),
|
|
221
230
|
short_description: getStringField(item, 'short_description'),
|
|
222
231
|
category: getStringField(item, 'category'),
|
|
@@ -232,7 +241,60 @@ async function showItem(app, sysID) {
|
|
|
232
241
|
standalone: standalone.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })),
|
|
233
242
|
sets: Array.from(setVars.values()).map(s => ({ name: s.name, variables: s.variables.map(v => ({ label: v.question_text || v.name, type: v.type, mandatory: String(v.mandatory) === 'true' })) })),
|
|
234
243
|
} : undefined,
|
|
235
|
-
}
|
|
236
|
-
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Styled-mode rendering (non-enumerable so JSON output stays clean)
|
|
247
|
+
Object.defineProperty(data, '_formatted', {
|
|
248
|
+
value: buildCatalogFormatted(data, standalone, Array.from(setVars.values()), totalVars),
|
|
249
|
+
enumerable: false,
|
|
250
|
+
configurable: true,
|
|
237
251
|
});
|
|
252
|
+
|
|
253
|
+
app.ok(data, {
|
|
254
|
+
summary: `${data.name} — ${totalVars} variable(s)`,
|
|
255
|
+
breadcrumbs: [
|
|
256
|
+
{ action: 'list', cmd: 'jsn catalogitems list', description: 'Back to all catalog items' },
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildCatalogFormatted(data, standalone, setVars, totalVars) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
lines.push(`${data.name} (sc_cat_item)`);
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('─ Details ─');
|
|
266
|
+
if (data.short_description) lines.push(` short_description: ${data.short_description}`);
|
|
267
|
+
if (data.category) lines.push(` category: ${data.category}`);
|
|
268
|
+
lines.push(` active: ${data.active}`);
|
|
269
|
+
if (data.url) lines.push(` url: ${data.url}`);
|
|
270
|
+
if (data.execution_plan) lines.push(` execution_plan: ${data.execution_plan}`);
|
|
271
|
+
lines.push('');
|
|
272
|
+
if (data.flow) {
|
|
273
|
+
lines.push('─ Flow ─');
|
|
274
|
+
lines.push(` ${data.flow}`);
|
|
275
|
+
if (data.flow_cmd) lines.push(` → ${data.flow_cmd}`);
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
if (data.workflow) {
|
|
279
|
+
lines.push('─ Workflow ─');
|
|
280
|
+
lines.push(` ${data.workflow}`);
|
|
281
|
+
if (data.workflow_cmd) lines.push(` → ${data.workflow_cmd}`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
}
|
|
284
|
+
if (totalVars > 0) {
|
|
285
|
+
lines.push('─ Catalog Variables ─');
|
|
286
|
+
for (const v of standalone) {
|
|
287
|
+
const label = v.question_text || v.name;
|
|
288
|
+
lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
|
|
289
|
+
}
|
|
290
|
+
for (const s of setVars) {
|
|
291
|
+
lines.push(` [${s.name}]`);
|
|
292
|
+
for (const v of s.variables) {
|
|
293
|
+
const label = v.question_text || v.name;
|
|
294
|
+
lines.push(` ${label}: ${v.type}${String(v.mandatory) === 'true' ? ' (mandatory)' : ''}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
}
|
|
299
|
+
return lines.join('\n');
|
|
238
300
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generic dev subcommand builder for table-based CRUD
|
|
2
2
|
|
|
3
|
-
import { formatRecordForDisplay, getStringField, isHexString, parseDataArg, confirmDelete, assertSafeExactMatch } from '../../helpers.js';
|
|
3
|
+
import { formatRecordForDisplay, getStringField, isHexString, parseDataArg, confirmDelete, assertSafeExactMatch, canPrompt } from '../../helpers.js';
|
|
4
4
|
import { getCurrentUser, getCurrentApplication } from '../../context.js';
|
|
5
5
|
import { paginatedSearch } from '../../paginated-search.js';
|
|
6
|
-
import {
|
|
6
|
+
import { FormatAuto } from '../../output.js';
|
|
7
7
|
|
|
8
8
|
function vowelArticle(word) {
|
|
9
9
|
const first = word.charAt(0).toLowerCase();
|
|
@@ -111,7 +111,7 @@ export function buildDevCmd(name, table, aliases, defaultColumns, wrap, opts = {
|
|
|
111
111
|
const limit = argv.limit;
|
|
112
112
|
|
|
113
113
|
// Interactive picker with pagination
|
|
114
|
-
const interactive =
|
|
114
|
+
const interactive = canPrompt();
|
|
115
115
|
if (interactive && app.output.getFormat() === FormatAuto && !query) {
|
|
116
116
|
// Get total count
|
|
117
117
|
let totalCount = 0;
|