@jacebenson/jsn 4.3.11 → 4.3.12-dev.70

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/src/helpers.js CHANGED
@@ -362,7 +362,7 @@ export function assertSafeExactMatch(value) {
362
362
  export async function interactiveList({ app, table, singular, columns, limit = 50, query = '', formatLabel, labelField = 'name', message, formatValue, promptFn }) {
363
363
  const prompt = promptFn || paginatedSearch;
364
364
  app.requireInstance();
365
- const effectiveFormat = app.output.getFormat() === FormatAuto ? (isTTY(process.stdout) ? FormatAuto : FormatAuto) : app.output.getFormat();
365
+ const effectiveFormat = app.output.getFormat();
366
366
  if (!promptFn && (effectiveFormat !== FormatAuto || !canPrompt())) {
367
367
  return null; // not interactive — caller should fall back to text/table
368
368
  }
@@ -522,6 +522,50 @@ export function parseDataArg(argv) {
522
522
  }
523
523
  }
524
524
 
525
+ /**
526
+ * Parse repeatable -f/--field name=value options.
527
+ * Values remain strings, including empty values and values containing '='.
528
+ */
529
+ export function parseFieldPairs(argv = {}) {
530
+ const raw = argv.field == null ? [] : (Array.isArray(argv.field) ? argv.field : [argv.field]);
531
+ const result = {};
532
+ for (const token of raw) {
533
+ const text = String(token);
534
+ const equals = text.indexOf('=');
535
+ if (equals < 0) throw new Error(`-f expects field=value, got "${text}"`);
536
+ const name = text.slice(0, equals);
537
+ let value = text.slice(equals + 1);
538
+ if (!name || /\\s/.test(name) || !/^[A-Za-z0-9_.]+$/.test(name)) {
539
+ throw new Error(`-f expects a valid field name, got "${name}"`);
540
+ }
541
+ if (value.startsWith('@@')) {
542
+ value = value.slice(1);
543
+ } else if (value.startsWith('@')) {
544
+ value = fs.readFileSync(value.slice(1), 'utf-8');
545
+ if (value.charCodeAt(0) === 0xFEFF) value = value.slice(1);
546
+ }
547
+ result[name] = value;
548
+ }
549
+ return result;
550
+ }
551
+
552
+ /**
553
+ * Build a record mutation payload from JSON input and/or field pairs.
554
+ */
555
+ export function buildRecordPayload(argv = {}, { required = true } = {}) {
556
+ const hasData = Boolean(argv.data || argv['data-file'] || argv['data-stdin']);
557
+ const hasFields = argv.field != null && (Array.isArray(argv.field) ? argv.field.length > 0 : true);
558
+ if (!hasData && !hasFields) {
559
+ if (required) throw new Error('Provide fields with -f name=value, or a JSON payload with --data/--data-file/--data-stdin');
560
+ return {};
561
+ }
562
+ const payload = hasData ? parseDataArg(argv) : {};
563
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
564
+ throw new Error('Record payload must be a JSON object');
565
+ }
566
+ return { ...payload, ...parseFieldPairs(argv) };
567
+ }
568
+
525
569
  /**
526
570
  * Translate human-readable type names to ServiceNow item_option_new type IDs.
527
571
  * Maps common names like "date", "select", "multilinetext" to their integer IDs.
@@ -2,89 +2,110 @@
2
2
  * Paginated search prompt built with @inquirer/core hooks.
3
3
  */
4
4
 
5
- import { createPrompt, useState, useKeypress, usePagination, useEffect, useRef } from '@inquirer/core';
6
- import { isEnterKey, isUpKey, isDownKey } from '@inquirer/core';
7
-
8
- export const paginatedSearch = createPrompt((config, done) => {
9
- const { message, pageSize = 10, totalCount = 0, source } = config;
10
-
11
- const [status, setStatus] = useState('loading');
12
- const [searchTerm, setSearchTerm] = useState('');
13
- const [choices, setChoices] = useState([]);
14
- const [active, setActive] = useState(0);
15
- const [loaded, setLoaded] = useState(0);
16
- const [searchMode, setSearchMode] = useState(false);
17
-
18
- // Refs for mutable state (no functional updaters — @inquirer/core doesn't support them)
19
- const choicesRef = useRef([]);
20
- const loadedRef = useRef(0);
21
- const activeRef = useRef(0);
22
- const totalCountRef = useRef(totalCount);
23
- const searchModeRef = useRef(false);
24
-
25
- const fullMessage = totalCount > 0
26
- ? `${message} (${loaded} of ${totalCount} loaded)`
27
- : message;
28
-
29
- // Load initial page
30
- useEffect(() => {
31
- const controller = new AbortController();
32
- setStatus('loading');
33
-
34
- const load = async () => {
35
- try {
36
- const items = await source(searchTerm || undefined, 0, { signal: controller.signal });
37
- if (!controller.signal.aborted) {
38
- choicesRef.current = items;
39
- loadedRef.current = items.length;
40
- activeRef.current = 0;
41
- setChoices(items);
42
- setLoaded(items.length);
43
- setActive(0);
44
- setStatus('done');
45
- }
46
- } catch {
47
- if (!controller.signal.aborted) {
48
- setStatus('done');
49
- setChoices([]);
50
- setLoaded(0);
5
+
6
+ export const paginatedSearch = async (...args) => {
7
+ const { createPrompt, useState, useKeypress, usePagination, useEffect, useRef, isEnterKey, isUpKey, isDownKey } = await import('@inquirer/core');
8
+ return createPrompt((config, done) => {
9
+ const { message, pageSize = 10, totalCount = 0, source } = config;
10
+
11
+ const [status, setStatus] = useState('loading');
12
+ const [searchTerm, setSearchTerm] = useState('');
13
+ const [choices, setChoices] = useState([]);
14
+ const [active, setActive] = useState(0);
15
+ const [loaded, setLoaded] = useState(0);
16
+ const [searchMode, setSearchMode] = useState(false);
17
+
18
+ // Refs for mutable state (no functional updaters — @inquirer/core doesn't support them)
19
+ const choicesRef = useRef([]);
20
+ const loadedRef = useRef(0);
21
+ const activeRef = useRef(0);
22
+ const totalCountRef = useRef(totalCount);
23
+ const searchModeRef = useRef(false);
24
+
25
+ const fullMessage = totalCount > 0
26
+ ? `${message} (${loaded} of ${totalCount} loaded)`
27
+ : message;
28
+
29
+ // Load initial page
30
+ useEffect(() => {
31
+ const controller = new AbortController();
32
+ setStatus('loading');
33
+
34
+ const load = async () => {
35
+ try {
36
+ const items = await source(searchTerm || undefined, 0, { signal: controller.signal });
37
+ if (!controller.signal.aborted) {
38
+ choicesRef.current = items;
39
+ loadedRef.current = items.length;
40
+ activeRef.current = 0;
41
+ setChoices(items);
42
+ setLoaded(items.length);
43
+ setActive(0);
44
+ setStatus('done');
45
+ }
46
+ } catch {
47
+ if (!controller.signal.aborted) {
48
+ setStatus('done');
49
+ setChoices([]);
50
+ setLoaded(0);
51
+ }
51
52
  }
53
+ };
54
+ load();
55
+ return () => controller.abort();
56
+ }, []);
57
+
58
+ // Load more when scrolling past end
59
+ async function loadMore() {
60
+ if (loadedRef.current >= totalCountRef.current || searchModeRef.current) return;
61
+ setStatus('loading');
62
+ try {
63
+ const newItems = await source('', loadedRef.current, { signal: new AbortController().signal });
64
+ const updated = choicesRef.current.concat(newItems);
65
+ choicesRef.current = updated;
66
+ loadedRef.current = updated.length;
67
+ setChoices([...updated]);
68
+ setLoaded(updated.length);
69
+ } catch (err) {
70
+ console.error('Scroll load error:', err.message || err);
71
+ loadedRef.current = totalCountRef.current;
72
+ setLoaded(totalCountRef.current);
52
73
  }
53
- };
54
- load();
55
- return () => controller.abort();
56
- }, []);
57
-
58
- // Load more when scrolling past end
59
- async function loadMore() {
60
- if (loadedRef.current >= totalCountRef.current || searchModeRef.current) return;
61
- setStatus('loading');
62
- try {
63
- const newItems = await source('', loadedRef.current, { signal: new AbortController().signal });
64
- const updated = choicesRef.current.concat(newItems);
65
- choicesRef.current = updated;
66
- loadedRef.current = updated.length;
67
- setChoices([...updated]);
68
- setLoaded(updated.length);
69
- } catch (err) {
70
- console.error('Scroll load error:', err.message || err);
71
- loadedRef.current = totalCountRef.current;
72
- setLoaded(totalCountRef.current);
74
+ setStatus('done');
73
75
  }
74
- setStatus('done');
75
- }
76
-
77
- // Search when term changes
78
- useEffect(() => {
79
- if (!searchTerm) {
80
- setSearchMode(false);
81
- searchModeRef.current = false;
76
+
77
+ // Search when term changes
78
+ useEffect(() => {
79
+ if (!searchTerm) {
80
+ setSearchMode(false);
81
+ searchModeRef.current = false;
82
+ const controller = new AbortController();
83
+ setStatus('loading');
84
+ source('', 0, { signal: controller.signal }).then(items => {
85
+ if (!controller.signal.aborted) {
86
+ choicesRef.current = items;
87
+ loadedRef.current = Math.min(items.length, totalCount || Infinity);
88
+ activeRef.current = 0;
89
+ setChoices(items);
90
+ setLoaded(items.length);
91
+ setActive(0);
92
+ setStatus('done');
93
+ }
94
+ }).catch(() => {
95
+ if (!controller.signal.aborted) setStatus('done');
96
+ });
97
+ return () => controller.abort();
98
+ }
99
+
82
100
  const controller = new AbortController();
83
101
  setStatus('loading');
84
- source('', 0, { signal: controller.signal }).then(items => {
102
+ setSearchMode(true);
103
+ searchModeRef.current = true;
104
+
105
+ source(searchTerm, undefined, { signal: controller.signal }).then(items => {
85
106
  if (!controller.signal.aborted) {
86
107
  choicesRef.current = items;
87
- loadedRef.current = Math.min(items.length, totalCount || Infinity);
108
+ loadedRef.current = items.length;
88
109
  activeRef.current = 0;
89
110
  setChoices(items);
90
111
  setLoaded(items.length);
@@ -92,103 +113,83 @@ export const paginatedSearch = createPrompt((config, done) => {
92
113
  setStatus('done');
93
114
  }
94
115
  }).catch(() => {
95
- if (!controller.signal.aborted) setStatus('done');
116
+ if (!controller.signal.aborted) {
117
+ setStatus('done');
118
+ setChoices([]);
119
+ }
96
120
  });
97
- return () => controller.abort();
98
- }
99
-
100
- const controller = new AbortController();
101
- setStatus('loading');
102
- setSearchMode(true);
103
- searchModeRef.current = true;
104
-
105
- source(searchTerm, undefined, { signal: controller.signal }).then(items => {
106
- if (!controller.signal.aborted) {
107
- choicesRef.current = items;
108
- loadedRef.current = items.length;
109
- activeRef.current = 0;
110
- setChoices(items);
111
- setLoaded(items.length);
112
- setActive(0);
113
- setStatus('done');
114
- }
115
- }).catch(() => {
116
- if (!controller.signal.aborted) {
117
- setStatus('done');
118
- setChoices([]);
119
- }
120
- });
121
121
 
122
- return () => controller.abort();
123
- }, [searchTerm]);
122
+ return () => controller.abort();
123
+ }, [searchTerm]);
124
124
 
125
- // Key handling
126
- useKeypress((key, rl) => {
127
- if (status === 'loading') return;
125
+ // Key handling
126
+ useKeypress((key, rl) => {
127
+ if (status === 'loading') return;
128
128
 
129
- if (isEnterKey(key)) {
130
- if (choicesRef.current.length > 0 && activeRef.current >= 0 && activeRef.current < choicesRef.current.length) {
131
- done(choicesRef.current[activeRef.current]);
129
+ if (isEnterKey(key)) {
130
+ if (choicesRef.current.length > 0 && activeRef.current >= 0 && activeRef.current < choicesRef.current.length) {
131
+ done(choicesRef.current[activeRef.current]);
132
+ }
133
+ return;
132
134
  }
133
- return;
134
- }
135
135
 
136
- if (key.name === 'escape') {
137
- done(undefined);
138
- return;
139
- }
136
+ if (key.name === 'escape') {
137
+ done(undefined);
138
+ return;
139
+ }
140
140
 
141
- if (isUpKey(key)) {
142
- if (activeRef.current > 0) {
143
- activeRef.current--;
144
- setActive(activeRef.current);
141
+ if (isUpKey(key)) {
142
+ if (activeRef.current > 0) {
143
+ activeRef.current--;
144
+ setActive(activeRef.current);
145
+ }
146
+ return;
145
147
  }
146
- return;
147
- }
148
148
 
149
- if (isDownKey(key)) {
150
- if (activeRef.current < choicesRef.current.length - 1) {
151
- activeRef.current++;
152
- setActive(activeRef.current);
153
- } else if (!searchModeRef.current && loadedRef.current < totalCountRef.current) {
154
- loadMore().then(() => {
149
+ if (isDownKey(key)) {
150
+ if (activeRef.current < choicesRef.current.length - 1) {
155
151
  activeRef.current++;
156
152
  setActive(activeRef.current);
157
- });
153
+ } else if (!searchModeRef.current && loadedRef.current < totalCountRef.current) {
154
+ loadMore().then(() => {
155
+ activeRef.current++;
156
+ setActive(activeRef.current);
157
+ });
158
+ }
159
+ return;
158
160
  }
159
- return;
160
- }
161
161
 
162
- // Use readline's built-in line buffer for text input
163
- const newTerm = rl.line;
164
- if (newTerm !== searchTerm) {
165
- setSearchTerm(newTerm);
166
- }
167
- });
168
-
169
- // Paginated rendering
170
- const page = usePagination({
171
- items: Array.isArray(choices) ? choices : [],
172
- active,
173
- pageSize,
174
- renderItem: ({ item, isActive }) => {
175
- const prefix = isActive ? '\x1b[7m' : ' ';
176
- const suffix = isActive ? '\x1b[0m' : '';
177
- const name = item.name || String(item.value || item);
178
- const display = name.length > 70 ? name.slice(0, 67) + '...' : name;
179
- return ` ${prefix}${display}${suffix}`;
180
- },
181
- loop: false,
182
- });
183
-
184
- const helpText = loaded < totalCount && !searchMode
185
- ? '(scroll to load more, type to search, Esc to cancel)'
186
- : '(type to search, Esc to cancel)';
187
-
188
- return [
189
- `${fullMessage} Search: ${searchTerm}${status === 'loading' ? ' ...' : ''}`,
190
- `${page}\n\n${helpText}`,
191
- ];
192
- });
162
+ // Use readline's built-in line buffer for text input
163
+ const newTerm = rl.line;
164
+ if (newTerm !== searchTerm) {
165
+ setSearchTerm(newTerm);
166
+ }
167
+ });
168
+
169
+ // Paginated rendering
170
+ const page = usePagination({
171
+ items: Array.isArray(choices) ? choices : [],
172
+ active,
173
+ pageSize,
174
+ renderItem: ({ item, isActive }) => {
175
+ const prefix = isActive ? '\x1b[7m' : ' ';
176
+ const suffix = isActive ? '\x1b[0m' : '';
177
+ const name = item.name || String(item.value || item);
178
+ const display = name.length > 70 ? name.slice(0, 67) + '...' : name;
179
+ return ` ${prefix}${display}${suffix}`;
180
+ },
181
+ loop: false,
182
+ });
183
+
184
+ const helpText = loaded < totalCount && !searchMode
185
+ ? '(scroll to load more, type to search, Esc to cancel)'
186
+ : '(type to search, Esc to cancel)';
187
+
188
+ return [
189
+ `${fullMessage} Search: ${searchTerm}${status === 'loading' ? ' ...' : ''}`,
190
+ `${page}\n\n${helpText}`,
191
+ ];
192
+ })(...args);
193
+ };
193
194
 
194
195
  export default paginatedSearch;
package/src/prompts.js CHANGED
@@ -1,5 +1,3 @@
1
- import * as prompts from '@inquirer/prompts';
2
-
3
1
  export class PromptCancelled extends Error {
4
2
  constructor() {
5
3
  super('Cancelled.');
@@ -30,14 +28,22 @@ export function cancellablePrompt(prompt) {
30
28
  };
31
29
  }
32
30
 
33
- export const input = cancellablePrompt(prompts.input);
34
- export const password = cancellablePrompt(prompts.password);
35
- export const select = cancellablePrompt(prompts.select);
36
- export const search = cancellablePrompt(prompts.search);
37
- export const confirm = cancellablePrompt(prompts.confirm);
38
- export const checkbox = cancellablePrompt(prompts.checkbox);
31
+ export const input = lazyPrompt('input');
32
+ export const password = lazyPrompt('password');
33
+ export const select = lazyPrompt('select');
34
+ export const search = lazyPrompt('search');
35
+ export const confirm = lazyPrompt('confirm');
36
+ export const checkbox = lazyPrompt('checkbox');
39
37
 
40
38
  // The editor prompt hands the keyboard to $EDITOR, so Escape belongs to that
41
39
  // editor. It lives here so every command imports prompts from one module;
42
40
  // cancelling an editor still uses Inquirer's Ctrl+C abort.
43
- export const editor = prompts.editor;
41
+ export const editor = async (...args) => (await import('@inquirer/prompts')).editor(...args);
42
+
43
+ // Load Inquirer only when a prompt is actually shown.
44
+ function lazyPrompt(name) {
45
+ return async (...args) => {
46
+ const prompts = await import('@inquirer/prompts');
47
+ return cancellablePrompt(prompts[name])(...args);
48
+ };
49
+ }