@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.
- package/README.md +60 -285
- package/package.json +5 -2
- package/scripts/postinstall.mjs +80 -0
- package/scripts/pre-commit-check.sh +61 -0
- package/scripts/release.js +62 -0
- package/scripts/tag-reminder.js +30 -0
- package/skills/servicenow/SKILL.md +47 -92
- package/src/app.js +5 -5
- package/src/cli.js +268 -161
- package/src/commands/_ticket.js +15 -2
- package/src/commands/auth.js +10 -1
- package/src/commands/catalog.js +179 -292
- package/src/commands/dev/_generic.js +50 -62
- package/src/commands/dev/_simple.js +27 -0
- package/src/commands/dev/b4rules.js +5 -0
- package/src/commands/dev/eval.js +1 -1
- package/src/commands/dev/flows.js +31 -8
- package/src/commands/dev/forms.js +131 -253
- package/src/commands/dev/lists.js +16 -6
- package/src/commands/dev/logs.js +52 -9
- package/src/commands/dev/scopes.js +4 -3
- package/src/commands/dev/scrapi.js +13 -13
- package/src/commands/dev/updatesets.js +6 -5
- 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 +16 -4
- 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 -35
- package/src/helpers.js +50 -34
- package/src/paginated-search.js +210 -0
- package/src/sdk.js +4 -0
|
@@ -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
|
}
|