@jacebenson/jsn 3.1.0 → 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.
- package/package.json +3 -2
- package/skills/servicenow/SKILL.md +41 -123
- package/src/cli.js +247 -200
- package/src/commands/_ticket.js +14 -1
- package/src/commands/auth.js +10 -1
- package/src/commands/catalog.js +179 -292
- package/src/commands/dev/_generic.js +39 -51
- package/src/commands/dev/_simple.js +27 -0
- package/src/commands/dev/b4rules.js +5 -0
- package/src/commands/dev/forms.js +123 -255
- package/src/commands/dev/logs.js +42 -9
- package/src/commands/groupmembers.js +10 -1
- package/src/commands/grouproles.js +10 -1
- package/src/commands/groups.js +12 -1
- package/src/commands/records.js +13 -1
- package/src/commands/skill.js +11 -1
- package/src/commands/tickets.js +12 -1
- package/src/commands/users.js +12 -1
- package/src/help.js +125 -78
- package/src/helpers.js +33 -20
- package/src/paginated-search.js +187 -155
- package/src/sdk.js +4 -0
package/src/paginated-search.js
CHANGED
|
@@ -1,178 +1,210 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paginated
|
|
3
|
-
*
|
|
4
|
-
* Uses raw readline for terminal control — @inquirer/search doesn't support
|
|
5
|
-
* scroll-based pagination (source only fires on keystrokes, not on scroll).
|
|
6
|
-
*
|
|
7
|
-
* Returns the selected choice ({name, value}), or null on cancel.
|
|
2
|
+
* Paginated search prompt built with @inquirer/core hooks.
|
|
8
3
|
*/
|
|
9
4
|
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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);
|
|
61
88
|
}
|
|
62
|
-
|
|
63
|
-
// Footer
|
|
64
|
-
const more = loaded < totalCount;
|
|
65
|
-
const footer = more
|
|
66
|
-
? `↑↓ navigate • ⏎ select • esc cancel • type to filter • ↓ at end loads more`
|
|
67
|
-
: `↑↓ navigate • ⏎ select • esc cancel • type to filter`;
|
|
68
|
-
buf.push('');
|
|
69
|
-
buf.push(footer);
|
|
70
|
-
|
|
71
|
-
// Search bar
|
|
72
|
-
buf.push(`> ${term}`);
|
|
73
|
-
|
|
74
|
-
outputLines = writeLines(buf);
|
|
89
|
+
setStatus('done');
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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();
|
|
82
113
|
}
|
|
83
|
-
// Ensure raw mode is off
|
|
84
|
-
try { process.stdin.setRawMode(false); } catch { /* ok */ }
|
|
85
|
-
}
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
135
|
});
|
|
93
|
-
try { process.stdin.setRawMode(true); } catch { /* non-TTY */ }
|
|
94
136
|
|
|
95
|
-
|
|
137
|
+
return () => controller.abort();
|
|
138
|
+
}, [searchTerm]);
|
|
96
139
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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]);
|
|
103
147
|
}
|
|
148
|
+
return;
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
choices = initial;
|
|
111
|
-
loaded = Math.min(choices.length, totalCount || Infinity);
|
|
112
|
-
active = 0;
|
|
113
|
-
render();
|
|
114
|
-
})();
|
|
115
|
-
|
|
116
|
-
rl.input.on('keypress', async (_str, key) => {
|
|
117
|
-
if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
118
|
-
cancelled = true;
|
|
119
|
-
rl.close();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
151
|
+
if (key.name === 'escape') {
|
|
152
|
+
done(undefined);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
122
155
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
156
|
+
if (isUpKey(key)) {
|
|
157
|
+
if (activeRef.current > 0) {
|
|
158
|
+
activeRef.current--;
|
|
159
|
+
setActive(activeRef.current);
|
|
128
160
|
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
});
|
|
134
174
|
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (active < choices.length - 1) active++;
|
|
143
|
-
render();
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
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
|
+
});
|
|
147
184
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
});
|
|
155
199
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const results = await source(term, 0);
|
|
160
|
-
choices = results;
|
|
161
|
-
loaded = results.length;
|
|
162
|
-
} else {
|
|
163
|
-
// Reset to browse mode — reload first page
|
|
164
|
-
const results = await source('', 0);
|
|
165
|
-
choices = results;
|
|
166
|
-
loaded = Math.min(results.length, totalCount || Infinity);
|
|
167
|
-
}
|
|
168
|
-
active = 0;
|
|
169
|
-
render();
|
|
170
|
-
});
|
|
200
|
+
const helpText = loaded < totalCount && !searchMode
|
|
201
|
+
? '(scroll to load more, type to search)'
|
|
202
|
+
: '(type to search)';
|
|
171
203
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
}
|