@jacebenson/jsn 3.0.6 → 3.1.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.
@@ -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) {
@@ -255,6 +255,19 @@ export function buildTicketCommands(table, displayName, alias, defaultColumns, s
255
255
  })
256
256
 
257
257
  },
258
- handler: () => {}, // Handled by subcommands
258
+ handler: () => {
259
+ const singular = displayName.endsWith('s') ? displayName.slice(0, -1) : displayName;
260
+ const article = singular.startsWith('a') || singular.startsWith('e') || singular.startsWith('i') || singular.startsWith('o') || singular.startsWith('u') ? 'an' : 'a';
261
+ console.log(`Manage ${displayName} from the ${table} table.`);
262
+ console.log('');
263
+ console.log('Available subcommands:');
264
+ console.log(` list List ${displayName}`);
265
+ console.log(` show <number or sys_id> Show ${article} ${singular} by number or sys_id`);
266
+ console.log(` create Create ${article} ${singular}`);
267
+ console.log(` update <identifier> Update ${article} ${singular}`);
268
+ console.log(` delete <identifier> Delete ${article} ${singular}`);
269
+ console.log('');
270
+ console.log(`Run "jsn ${displayName} <command> --help" for details.`);
271
+ }, // Default handler
259
272
  };
260
273
  }
@@ -302,6 +302,15 @@ Examples:
302
302
  }),
303
303
  });
304
304
  },
305
- handler: () => {},
305
+ handler: () => {
306
+ console.log('Manage OAuth authentication for ServiceNow instances.');
307
+ console.log('');
308
+ console.log('Available subcommands:');
309
+ console.log(' login <url> Login with OAuth (supports --print-url, --code for CI/CD)');
310
+ console.log(' status Show auth status for the active profile');
311
+ console.log(' refresh Refresh the access token');
312
+ console.log('');
313
+ console.log('Run "jsn auth <command> --help" for details.');
314
+ },
306
315
  };
307
316
  }
@@ -1,325 +1,212 @@
1
- // Catalog item management commands
2
- // NOTE: This is an AI-friendly high-level command that wraps sc_cat_item + item_option_new
3
-
4
- import { resolveItemOptionType } from '../helpers.js';
1
+ import { getStringField, interactiveList } from '../helpers.js';
5
2
 
6
3
  export function catalogCmd(wrap) {
7
4
  return {
8
- command: 'catalog <subcommand>',
9
- aliases: ['cat'],
10
- describe: 'Manage Service Catalog items and variables',
5
+ command: 'catalogitems [subcommand]',
6
+ aliases: ['ci'],
7
+ describe: 'Manage Service Catalog items',
11
8
  builder: (yargs) => {
12
9
  return yargs
13
- // ── create-item: Create a new catalog item definition ──
14
10
  .command({
15
- command: 'create-item',
16
- describe: 'Create a catalog item (sc_cat_item) with variables',
11
+ command: 'create',
12
+ aliases: ['new'],
13
+ describe: 'Create a catalog item',
17
14
  builder: (y) => y
18
- .option('name', { alias: 'n', type: 'string', demandOption: true, describe: 'Catalog item name' })
19
- .option('short-description', { alias: 'd', type: 'string', describe: 'Short description' })
20
- .option('description', { type: 'string', describe: 'Full description' })
21
- .option('category', { alias: 'c', type: 'string', describe: 'Category name (created if missing)' })
22
- .option('variable', { alias: 'v', type: 'array', describe: 'Variable definition in format "name:type:label" or "name:type" (e.g. "reason:multilinetext:Reason" or "start_date:date")' })
23
- .option('variables', { type: 'string', describe: 'JSON object of variable values for submit_produce API (e.g. \'{"quantity":"100","reason":"Need more"}\')' })
24
- .option('update-set', { type: 'string', describe: 'Update set sys_id or name to capture changes' }),
15
+ .option('name', { alias: 'n', type: 'string', demandOption: true })
16
+ .option('short-description', { alias: 'd', type: 'string' })
17
+ .option('category', { alias: 'c', type: 'string' }),
25
18
  handler: wrap(async (argv, app) => {
26
- // If --variables is provided, use submit_produce API (request against existing item)
27
- if (argv.variables) {
28
- let varsObj;
29
- try {
30
- varsObj = JSON.parse(argv.variables);
31
- } catch {
32
- throw new Error('--variables must be a valid JSON object, e.g. {"reason":"Need this"}');
33
- }
34
- // Resolve the catalog item sys_id from name if needed
35
- let itemSysID = argv.name;
36
- if (!itemSysID.match(/^[a-f0-9]{32}$/i)) {
37
- const params = new URLSearchParams();
38
- params.set('sysparm_query', `name=${itemSysID}`);
39
- params.set('sysparm_limit', '1');
40
- params.set('sysparm_fields', 'sys_id');
41
- const items = await app.sdk.list('sc_cat_item', params);
42
- if (items.length === 0) throw new Error(`Catalog item not found: ${itemSysID}`);
43
- const item = items[0];
44
- itemSysID = item.sys_id?.value || item.sys_id;
45
- }
46
- // Submit via submit_produce API
47
- const endpoint = `${app.sdk.baseURL}/api/sn_sc/servicecatalog/items/${itemSysID}/submit_produce`;
48
- const body = { variables: varsObj };
49
- const result = await app.sdk.request(endpoint, {
50
- method: 'POST',
51
- body: JSON.stringify(body),
52
- });
53
- const reqID = result?.result?.sys_id || result?.result?.number || '';
54
- app.ok({
55
- requested_item: reqID,
56
- item_id: itemSysID,
57
- variables: varsObj,
58
- }, {
59
- summary: `Request created: ${result?.result?.number || reqID}`,
60
- breadcrumbs: [
61
- { action: 'view', cmd: `jsn requests show ${reqID}`, description: 'View the request' },
62
- ],
63
- });
64
- return;
65
- }
66
-
67
- // Original create-item flow: create a new catalog item definition
68
- const name = argv.name;
69
- const shortDesc = argv['short-description'] || '';
70
- const description = argv.description || shortDesc;
19
+ app.requireInstance();
71
20
  let categoryID = '';
72
-
73
- // Resolve or create category
74
21
  if (argv.category) {
75
- const catParams = new URLSearchParams();
76
- catParams.set('sysparm_query', `title=${argv.category}`);
77
- catParams.set('sysparm_limit', '1');
78
- catParams.set('sysparm_fields', 'sys_id,title');
79
- const cats = await app.sdk.list('sc_category', catParams);
80
- if (cats.length > 0) {
81
- categoryID = cats[0].sys_id?.value || cats[0].sys_id;
82
- } else {
83
- const newCat = await app.sdk.create('sc_category', {
84
- title: argv.category,
85
- description: `Category for ${argv.category}`,
86
- });
87
- categoryID = newCat.sys_id?.value || newCat.sys_id;
88
- }
22
+ const cp = new URLSearchParams();
23
+ cp.set('sysparm_query', `title=${argv.category}`);
24
+ cp.set('sysparm_limit', '1');
25
+ cp.set('sysparm_fields', 'sys_id');
26
+ const cats = await app.sdk.list('sc_category', cp);
27
+ if (cats.length > 0) categoryID = cats[0].sys_id?.value || cats[0].sys_id;
28
+ else { const nc = await app.sdk.create('sc_category', { title: argv.category }); categoryID = nc.sys_id?.value || nc.sys_id; }
89
29
  }
90
-
91
- // Create the catalog item
92
30
  const item = await app.sdk.create('sc_cat_item', {
93
- name,
94
- short_description: shortDesc,
95
- description,
31
+ name: argv.name,
32
+ short_description: argv['short-description'] || '',
96
33
  category: categoryID || undefined,
97
34
  active: true,
98
35
  type: 'item',
99
- stage: 'requested',
100
- });
101
- const itemID = item.sys_id?.value || item.sys_id;
102
-
103
- // Parse and create variables
104
- const varDefs = [];
105
- if (argv.variable) {
106
- for (const v of argv.variable) {
107
- const parts = String(v).split(':');
108
- if (parts.length >= 2) {
109
- const typeName = parts.length >= 2 ? parts[parts.length - 2] : '';
110
- const label = parts.length >= 3 ? parts[parts.length - 1] : parts[0];
111
- const typeID = resolveItemOptionType(typeName);
112
- const sysName = parts[0].replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
113
- varDefs.push({
114
- name: sysName,
115
- question_text: label,
116
- type: typeID,
117
- order: varDefs.length * 100 + 100,
118
- cat_item: itemID,
119
- mandatory: true,
120
- active: true,
121
- });
122
- }
123
- }
124
-
125
- for (const vd of varDefs) {
126
- try {
127
- await app.sdk.create('item_option_new', vd);
128
- } catch {
129
- // variable creation failure is non-fatal
130
- }
131
- }
132
- }
133
-
134
- app.ok({
135
- sys_id: itemID,
136
- name,
137
- category: argv.category || null,
138
- variables_created: varDefs.length,
139
- update_set: argv['update-set'] || null,
140
- instance_url: app.getEffectiveInstance(),
141
- }, {
142
- summary: `Created catalog item: ${name} (${varDefs.length} variable(s))`,
143
- breadcrumbs: [
144
- { action: 'view', cmd: `jsn records get --table sc_cat_item --sys-id ${itemID}`, description: 'View the catalog item' },
145
- { action: 'edit', cmd: `jsn catalog update-item ${itemID} --name "${name}"`, description: 'Edit the catalog item' },
146
- ],
147
36
  });
37
+ app.ok(item, { summary: `Created: ${argv.name}` });
148
38
  }),
149
39
  })
150
- // ── items: List and show catalog items ──
151
40
  .command({
152
- command: 'items <subcommand>',
153
- aliases: ['item'],
154
- describe: 'Browse catalog items and inspect variables',
41
+ command: 'list',
42
+ aliases: ['ls'],
43
+ describe: 'List catalog items',
155
44
  builder: (y) => y
156
- .command({
157
- command: 'list',
158
- aliases: ['ls'],
159
- describe: 'List catalog items',
160
- builder: (y) => y
161
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
162
- .option('category', { type: 'string', describe: 'Filter by category' }),
163
- handler: wrap(async (argv, app) => {
164
- let query = 'ORDERBYname';
165
- if (argv.category) query = `category.titleLIKE${argv.category}^${query}`;
166
- const params = new URLSearchParams();
167
- params.set('sysparm_query', query);
168
- params.set('sysparm_limit', String(argv.limit));
169
- params.set('sysparm_display_value', 'all');
170
- params.set('sysparm_fields', 'sys_id,name,short_description,category,active');
171
- const items = await app.sdk.list('sc_cat_item', params);
172
- app.ok({
173
- table: 'sc_cat_item',
174
- count: items.length,
175
- columns: ['name', 'short_description', 'category', 'active'],
176
- records: items.map(r => ({
177
- sys_id: r.sys_id?.value || r.sys_id,
178
- name: r.name?.display_value || r.name,
179
- short_description: r.short_description?.display_value || r.short_description || '',
180
- category: r.category?.display_value || '',
181
- active: r.active?.display_value || r.active,
182
- })),
183
- context: { instance_url: app.getEffectiveInstance() },
184
- }, { summary: `${items.length} catalog item(s)` });
185
- }),
186
- })
187
- .command({
188
- command: 'show <id>',
189
- describe: 'Show a catalog item with variables and variable sets',
190
- handler: wrap(async (argv, app) => {
191
- let itemID = argv.id;
192
- // Resolve name to sys_id
193
- if (!itemID.match(/^[a-f0-9]{32}$/i)) {
194
- const params = new URLSearchParams();
195
- params.set('sysparm_query', `name=${itemID}`);
196
- params.set('sysparm_limit', '1');
197
- params.set('sysparm_fields', 'sys_id,name');
198
- const items = await app.sdk.list('sc_cat_item', params);
199
- if (items.length === 0) throw new Error(`Catalog item not found: ${itemID}`);
200
- itemID = items[0].sys_id?.value || items[0].sys_id;
201
- }
202
-
203
- // Get the catalog item
204
- const item = await app.sdk.get('sc_cat_item', itemID);
205
-
206
- // Get variables (item_option_new) for this catalog item
207
- const varParams = new URLSearchParams();
208
- varParams.set('sysparm_query', `cat_item=${itemID}^active=true`);
209
- varParams.set('sysparm_limit', '100');
210
- varParams.set('sysparm_display_value', 'all');
211
- varParams.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
212
- const variables = await app.sdk.list('item_option_new', varParams);
45
+ .option('limit', { alias: 'l', type: 'number', default: 50 })
46
+ .option('category', { type: 'string' }),
47
+ handler: wrap(async (argv, app) => {
48
+ app.requireInstance();
49
+ const query = argv.category ? `category.titleLIKE${argv.category}` : '';
213
50
 
214
- // Separate standalone vs variable-set variables
215
- const standaloneVars = [];
216
- const setVars = new Map(); // set_sys_id -> { name, label, variables: [] }
217
- for (const v of variables) {
218
- const vs = v.variable_set?.value || v.variable_set;
219
- const entry = {
220
- sys_id: v.sys_id?.value || v.sys_id,
221
- name: v.name?.value || v.name || '',
222
- question_text: v.question_text?.display_value || v.question_text || '',
223
- type: v.type?.display_value || v.type,
224
- order: v.order?.display_value || v.order,
225
- mandatory: v.mandatory?.display_value || v.mandatory,
226
- };
227
- if (vs) {
228
- if (!setVars.has(vs)) {
229
- setVars.set(vs, { name: vs, label: vs, variables: [] });
230
- }
231
- setVars.get(vs).variables.push(entry);
232
- } else {
233
- standaloneVars.push(entry);
234
- }
235
- }
51
+ const picked = await interactiveList({
52
+ app,
53
+ table: 'sc_cat_item',
54
+ singular: 'catalog item',
55
+ columns: ['name', 'short_description', 'category', 'active'],
56
+ limit: argv.limit,
57
+ query,
58
+ labelField: 'name',
59
+ formatLabel: (r) => {
60
+ const n = getStringField(r, 'name') || '';
61
+ const c = getStringField(r, 'category') || '';
62
+ return c ? `${n} [${c}]` : n;
63
+ },
64
+ });
236
65
 
237
- // Try to resolve variable set names
238
- for (const [sysID, setData] of setVars) {
239
- try {
240
- const vsRecord = await app.sdk.get('sc_cat_item_option_set', sysID);
241
- setData.name = vsRecord.name?.value || vsRecord.name || sysID;
242
- setData.label = vsRecord.label?.value || vsRecord.label || setData.name;
243
- setData.order = vsRecord.order?.display_value || vsRecord.order;
244
- } catch {
245
- // Can't resolve, keep sys_id as name
246
- }
247
- }
66
+ if (picked === undefined) return;
67
+ if (picked) {
68
+ await showItem(app, getStringField(picked, 'sys_id') || '');
69
+ return;
70
+ }
248
71
 
249
- app.ok({
250
- sys_id: itemID,
251
- name: item.name?.display_value || item.name,
252
- short_description: item.short_description?.display_value || item.short_description || '',
253
- description: item.description?.display_value || item.description || '',
254
- category: item.category?.display_value || '',
255
- active: item.active?.display_value || item.active,
256
- variables: {
257
- standalone: standaloneVars,
258
- variable_sets: Array.from(setVars.values()),
259
- },
260
- instance_url: app.getEffectiveInstance(),
261
- }, {
262
- summary: `${item.name?.display_value || item.name} — ${standaloneVars.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0)} variable(s)`,
263
- });
264
- }),
265
- })
266
- .demandCommand(1, 'Specify a subcommand: list or show'),
267
- handler: () => {},
268
- })
269
- // ── list-items: Legacy alias ──
270
- .command({
271
- command: 'list-items',
272
- aliases: ['ls'],
273
- describe: 'List catalog items (use "catalog items list" instead)',
274
- builder: (y) => y
275
- .option('limit', { alias: 'l', type: 'number', default: 20, describe: 'Max records' })
276
- .option('category', { type: 'string', describe: 'Filter by category' }),
277
- handler: wrap(async (argv, app) => {
278
- let query = 'ORDERBYname';
279
- if (argv.category) query = `category.titleLIKE${argv.category}^${query}`;
280
- const params = new URLSearchParams();
281
- params.set('sysparm_query', query);
282
- params.set('sysparm_limit', String(argv.limit));
283
- params.set('sysparm_display_value', 'all');
284
- params.set('sysparm_fields', 'sys_id,name,short_description,category,active');
285
- const items = await app.sdk.list('sc_cat_item', params);
72
+ // Fallback — non-TTY
73
+ const p = new URLSearchParams();
74
+ p.set('sysparm_query', query ? `${query}^ORDERBYname` : 'ORDERBYname');
75
+ p.set('sysparm_limit', String(argv.limit));
76
+ p.set('sysparm_display_value', 'all');
77
+ p.set('sysparm_fields', 'name,short_description,category,active');
78
+ const records = await app.sdk.list('sc_cat_item', p);
286
79
  app.ok({
287
80
  table: 'sc_cat_item',
288
- count: items.length,
81
+ count: records.length,
289
82
  columns: ['name', 'short_description', 'category', 'active'],
290
- records: items.map(r => ({
291
- sys_id: r.sys_id?.value || r.sys_id,
292
- name: r.name?.display_value || r.name,
293
- short_description: r.short_description?.display_value || r.short_description || '',
294
- category: r.category?.display_value || '',
295
- active: r.active?.display_value || r.active,
296
- })),
83
+ records,
297
84
  context: { instance_url: app.getEffectiveInstance() },
298
- }, { summary: `${items.length} catalog item(s)` });
85
+ }, { summary: `${records.length} catalog item(s)` });
86
+ }),
87
+ })
88
+ .command({
89
+ command: 'show <id>',
90
+ aliases: ['get'],
91
+ describe: 'Show a catalog item',
92
+ handler: wrap(async (argv, app) => {
93
+ app.requireInstance();
94
+ let id = argv.id;
95
+ if (!/^[a-f0-9]{32}$/i.test(id)) {
96
+ const p = new URLSearchParams();
97
+ p.set('sysparm_query', `name=${id}`);
98
+ p.set('sysparm_limit', '1');
99
+ p.set('sysparm_fields', 'sys_id');
100
+ const results = await app.sdk.list('sc_cat_item', p);
101
+ if (results.length === 0) throw new Error(`Not found: ${id}`);
102
+ id = results[0].sys_id?.value || results[0].sys_id;
103
+ }
104
+ const item = await app.sdk.get('sc_cat_item', id);
105
+ await showItem(app, id);
299
106
  }),
300
107
  });
301
108
  },
302
- handler: (argv) => {
303
- if (!argv._[1]) {
304
- console.log('Manage Service Catalog items.\n');
305
- console.log('Commands:');
306
- console.log(' create-item Create a catalog item, or submit a request via submit_produce');
307
- console.log(' items list List catalog items');
308
- console.log(' items show Show catalog item details with variables');
309
- console.log('\nExamples:');
310
- console.log(' jsn catalog create-item "Request PTO" \\');
311
- console.log(' --category "Time Off" \\');
312
- console.log(' --short-description "Submit a PTO request" \\');
313
- console.log(' --variable "start_date:date:Start Date" \\');
314
- console.log(' --variable "type:select:Type" \\');
315
- console.log(' --variable "reason:multilinetext:Reason/Notes"');
316
- console.log('');
317
- console.log(' # Submit a request via submit_produce:');
318
- console.log(' jsn catalog create-item "Minecraft Coins" \\');
319
- console.log(' --variables \'{"quantity":"100","reason":"Server upgrade"}\'');
320
- console.log('\nVariable types: string, multilinetext, select, date, datetime, reference, checkbox, email');
321
- console.log('\nRun "jsn catalog <command> --help" for details.');
322
- }
109
+ handler: () => {
110
+ console.log('Service Catalog management.');
111
+ console.log('');
112
+ console.log('Commands:');
113
+ console.log(' create Create a catalog item');
114
+ console.log(' list Browse catalog items');
115
+ console.log(' show <id> Show catalog item');
323
116
  },
324
117
  };
325
118
  }
119
+
120
+ async function showItem(app, sysID) {
121
+ const item = await app.sdk.get('sc_cat_item', sysID);
122
+
123
+ // Fetch variables
124
+ const vp = new URLSearchParams();
125
+ vp.set('sysparm_query', `cat_item=${sysID}^active=true`);
126
+ vp.set('sysparm_limit', '100');
127
+ vp.set('sysparm_display_value', 'all');
128
+ vp.set('sysparm_fields', 'sys_id,name,question_text,type,order,mandatory,variable_set');
129
+ const variables = await app.sdk.list('item_option_new', vp);
130
+
131
+ const standalone = [];
132
+ const setVars = new Map();
133
+ for (const v of variables) {
134
+ const vs = getStringField(v, 'variable_set');
135
+ const entry = {
136
+ name: getStringField(v, 'name'),
137
+ question_text: getStringField(v, 'question_text'),
138
+ type: getStringField(v, 'type'),
139
+ mandatory: getStringField(v, 'mandatory'),
140
+ };
141
+ if (vs) {
142
+ if (!setVars.has(vs)) setVars.set(vs, { name: vs, variables: [] });
143
+ setVars.get(vs).variables.push(entry);
144
+ } else {
145
+ standalone.push(entry);
146
+ }
147
+ }
148
+
149
+ const totalVars = standalone.length + Array.from(setVars.values()).reduce((a, s) => a + s.variables.length, 0);
150
+
151
+ const lines = [];
152
+ lines.push(getStringField(item, 'name'));
153
+ if (getStringField(item, 'short_description')) lines.push(` ${getStringField(item, 'short_description')}`);
154
+ lines.push(` Category: ${getStringField(item, 'category') || '(none)'}`);
155
+ lines.push(` Active: ${getStringField(item, 'active') || 'false'}`);
156
+ lines.push(` URL: ${app.getEffectiveInstance()}/sc_cat_item.do?sys_id=${sysID}`);
157
+
158
+ // Flow / Workflow / Execution Plan
159
+ const flowName = getStringField(item, 'flow_designer_flow');
160
+ const flowID = item.flow_designer_flow?.value || '';
161
+ const wfName = getStringField(item, 'workflow');
162
+ const wfID = item.workflow?.value || '';
163
+ const planName = getStringField(item, 'delivery_plan') || getStringField(item, 'execution_plan');
164
+
165
+ if (flowID) {
166
+ if (flowName === flowID) {
167
+ try {
168
+ const flowRec = await app.sdk.get('sys_hub_flow', flowID);
169
+ lines.push(` Flow: ${getStringField(flowRec, 'name') || flowID}`);
170
+ } catch {
171
+ lines.push(` Flow: ${flowName}`);
172
+ }
173
+ } else {
174
+ lines.push(` Flow: ${flowName}`);
175
+ }
176
+ lines.push(` → jsn flows show ${flowID}`);
177
+ } else if (wfName) {
178
+ lines.push(` Workflow: ${wfName}`);
179
+ if (wfID) lines.push(` → jsn workflows show ${wfID}`);
180
+ } else if (planName) {
181
+ const planID = item.delivery_plan?.value || item.execution_plan?.value || '';
182
+ if (planName === planID && planID) {
183
+ try {
184
+ const planRec = await app.sdk.get('delivery_plan', planID);
185
+ const realName = getStringField(planRec, 'name') || getStringField(planRec, 'short_description') || planID;
186
+ lines.push(` Execution Plan: ${realName}`);
187
+ lines.push(` → jsn records get --table delivery_plan --sys-id ${planID}`);
188
+ } catch {
189
+ lines.push(` Execution Plan: ${planName}`);
190
+ }
191
+ } else {
192
+ lines.push(` Execution Plan: ${planName}`);
193
+ }
194
+ }
195
+
196
+ if (totalVars > 0) {
197
+ lines.push(` Variables (${totalVars}):`);
198
+ for (const v of standalone) {
199
+ const flags = String(v.mandatory) === 'true' ? ' [required]' : '';
200
+ lines.push(` ${v.question_text || v.name} ${v.type || ''}${flags}`);
201
+ }
202
+ for (const [_, sd] of setVars) {
203
+ lines.push(` [${sd.name}]`);
204
+ for (const v of sd.variables) {
205
+ const flags = String(v.mandatory) === 'true' ? ' [required]' : '';
206
+ lines.push(` ${v.question_text || v.name} ${v.type || ''}${flags}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ console.log(lines.join('\n'));
212
+ }