@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.
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Paginated search prompt built with @inquirer/core hooks.
3
+ */
4
+
5
+ import { createPrompt, useState, useKeypress, usePagination, useEffect, useRef } from '@inquirer/core';
6
+ import { isEnterKey, isUpKey, isDownKey } from '@inquirer/core';
7
+ import { appendFileSync } from 'fs';
8
+
9
+ export const paginatedSearch = createPrompt((config, done) => {
10
+ const { message, pageSize = 10, totalCount = 0, source } = config;
11
+
12
+ // ── DEBUG ──
13
+ const DBG = '/home/jace/workspace/holly/sn-jsn-fork/debug.log';
14
+ const log = (msg) => appendFileSync(DBG, `${new Date().toISOString()} ${msg}\n`);
15
+ log(`START totalCount=${totalCount}`);
16
+
17
+ const [status, setStatus] = useState('loading');
18
+ const [searchTerm, setSearchTerm] = useState('');
19
+ const [choices, setChoices] = useState([]);
20
+ const [active, setActive] = useState(0);
21
+ const [loaded, setLoaded] = useState(0);
22
+ const [searchMode, setSearchMode] = useState(false);
23
+
24
+ // Refs for mutable state (no functional updaters — @inquirer/core doesn't support them)
25
+ const choicesRef = useRef([]);
26
+ const loadedRef = useRef(0);
27
+ const activeRef = useRef(0);
28
+ const totalCountRef = useRef(totalCount);
29
+ const searchModeRef = useRef(false);
30
+
31
+ const fullMessage = totalCount > 0
32
+ ? `${message} (${loaded} of ${totalCount} loaded)`
33
+ : message;
34
+
35
+ // Load initial page
36
+ useEffect(() => {
37
+ const controller = new AbortController();
38
+ setStatus('loading');
39
+ log(`EFFECT-INIT searchTerm="${searchTerm}"`);
40
+
41
+ const load = async () => {
42
+ try {
43
+ const items = await source(searchTerm || undefined, 0, { signal: controller.signal });
44
+ log(`INIT-LOAD got ${items.length} items`);
45
+ if (!controller.signal.aborted) {
46
+ choicesRef.current = items;
47
+ loadedRef.current = items.length;
48
+ activeRef.current = 0;
49
+ setChoices(items);
50
+ setLoaded(items.length);
51
+ setActive(0);
52
+ setStatus('done');
53
+ }
54
+ } catch (err) {
55
+ log(`INIT-LOAD error: ${err.message}`);
56
+ if (!controller.signal.aborted) {
57
+ setStatus('done');
58
+ setChoices([]);
59
+ setLoaded(0);
60
+ }
61
+ }
62
+ };
63
+ load();
64
+ return () => controller.abort();
65
+ }, []);
66
+
67
+ // Load more when scrolling past end
68
+ async function loadMore() {
69
+ log(`loadMore check: loaded=${loadedRef.current} total=${totalCountRef.current} searchMode=${searchModeRef.current}`);
70
+ if (loadedRef.current >= totalCountRef.current || searchModeRef.current) {
71
+ log(`loadMore SKIPPED`);
72
+ return;
73
+ }
74
+ setStatus('loading');
75
+ log(`loadMore calling source offset=${loadedRef.current}`);
76
+ try {
77
+ const newItems = await source('', loadedRef.current, { signal: new AbortController().signal });
78
+ log(`loadMore got ${newItems.length} items`);
79
+ const updated = choicesRef.current.concat(newItems);
80
+ choicesRef.current = updated;
81
+ loadedRef.current = updated.length;
82
+ setChoices([...updated]);
83
+ setLoaded(updated.length);
84
+ } catch (err) {
85
+ console.error('Scroll load error:', err.message || err);
86
+ loadedRef.current = totalCountRef.current;
87
+ setLoaded(totalCountRef.current);
88
+ }
89
+ setStatus('done');
90
+ }
91
+
92
+ // Search when term changes
93
+ useEffect(() => {
94
+ if (!searchTerm) {
95
+ setSearchMode(false);
96
+ searchModeRef.current = false;
97
+ const controller = new AbortController();
98
+ setStatus('loading');
99
+ source('', 0, { signal: controller.signal }).then(items => {
100
+ if (!controller.signal.aborted) {
101
+ choicesRef.current = items;
102
+ loadedRef.current = Math.min(items.length, totalCount || Infinity);
103
+ activeRef.current = 0;
104
+ setChoices(items);
105
+ setLoaded(items.length);
106
+ setActive(0);
107
+ setStatus('done');
108
+ }
109
+ }).catch(() => {
110
+ if (!controller.signal.aborted) setStatus('done');
111
+ });
112
+ return () => controller.abort();
113
+ }
114
+
115
+ const controller = new AbortController();
116
+ setStatus('loading');
117
+ setSearchMode(true);
118
+ searchModeRef.current = true;
119
+
120
+ source(searchTerm, undefined, { signal: controller.signal }).then(items => {
121
+ if (!controller.signal.aborted) {
122
+ choicesRef.current = items;
123
+ loadedRef.current = items.length;
124
+ activeRef.current = 0;
125
+ setChoices(items);
126
+ setLoaded(items.length);
127
+ setActive(0);
128
+ setStatus('done');
129
+ }
130
+ }).catch(() => {
131
+ if (!controller.signal.aborted) {
132
+ setStatus('done');
133
+ setChoices([]);
134
+ }
135
+ });
136
+
137
+ return () => controller.abort();
138
+ }, [searchTerm]);
139
+
140
+ // Key handling
141
+ useKeypress((key, rl) => {
142
+ if (status === 'loading') return;
143
+
144
+ if (isEnterKey(key)) {
145
+ if (choicesRef.current.length > 0 && activeRef.current >= 0 && activeRef.current < choicesRef.current.length) {
146
+ done(choicesRef.current[activeRef.current]);
147
+ }
148
+ return;
149
+ }
150
+
151
+ if (key.name === 'escape') {
152
+ done(undefined);
153
+ return;
154
+ }
155
+
156
+ if (isUpKey(key)) {
157
+ if (activeRef.current > 0) {
158
+ activeRef.current--;
159
+ setActive(activeRef.current);
160
+ }
161
+ return;
162
+ }
163
+
164
+ if (isDownKey(key)) {
165
+ if (activeRef.current < choicesRef.current.length - 1) {
166
+ activeRef.current++;
167
+ setActive(activeRef.current);
168
+ } else if (!searchModeRef.current && loadedRef.current < totalCountRef.current) {
169
+ log(`DOWN at end: active=${activeRef.current} len=${choicesRef.current.length} loaded=${loadedRef.current} total=${totalCountRef.current}`);
170
+ loadMore().then(() => {
171
+ activeRef.current++;
172
+ setActive(activeRef.current);
173
+ });
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Use readline's built-in line buffer for text input
179
+ const newTerm = rl.line;
180
+ if (newTerm !== searchTerm) {
181
+ setSearchTerm(newTerm);
182
+ }
183
+ });
184
+
185
+ // Paginated rendering
186
+ const page = usePagination({
187
+ items: Array.isArray(choices) ? choices : [],
188
+ active,
189
+ pageSize,
190
+ renderItem: ({ item, isActive }) => {
191
+ const prefix = isActive ? '\x1b[7m' : ' ';
192
+ const suffix = isActive ? '\x1b[0m' : '';
193
+ const name = item.name || String(item.value || item);
194
+ const display = name.length > 70 ? name.slice(0, 67) + '...' : name;
195
+ return ` ${prefix}${display}${suffix}`;
196
+ },
197
+ loop: false,
198
+ });
199
+
200
+ const helpText = loaded < totalCount && !searchMode
201
+ ? '(scroll to load more, type to search)'
202
+ : '(type to search)';
203
+
204
+ return [
205
+ `${fullMessage} Search: ${searchTerm}${status === 'loading' ? ' ...' : ''}`,
206
+ `${page}\n\n${helpText}`,
207
+ ];
208
+ });
209
+
210
+ export default paginatedSearch;
package/src/sdk.js CHANGED
@@ -139,6 +139,10 @@ export class SDKClient {
139
139
  async list(table, params = {}) {
140
140
  const query = new URLSearchParams(params).toString();
141
141
  const endpoint = `${this.baseURL}/api/now/table/${table}${query ? '?' + query : ''}`;
142
+ // Debug: log paginated queries
143
+ if (params.sysparm_offset) {
144
+ process.stderr.write(`[SDK list] offset=${params.sysparm_offset} limit=${params.sysparm_limit} table=${table}\n`);
145
+ }
142
146
  const result = await this.request(endpoint, { method: 'GET' });
143
147
  return result?.result || [];
144
148
  }