@jacebenson/jsn 4.3.10 → 4.3.11-dev.69

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.
@@ -1,4 +1,3 @@
1
- import { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } from '@inquirer/core';
2
1
  import { loginWizard, loginProfile, showAuthStatus, switchProfile, modifyProfile, logoutProfile, removeConnection } from './auth.js';
3
2
  import { errUsage } from '../errors.js';
4
3
  import { declareCapabilities } from '../capabilities.js';
@@ -18,32 +17,35 @@ const HUB_CHOICES = [
18
17
  ];
19
18
 
20
19
  /** The setup hub prompt; q and Escape resolve with no selected action. */
21
- export const authHubPrompt = createPrompt((config, done) => {
22
- const [active, setActive] = useState(0);
23
- const choices = config.choices;
20
+ export const authHubPrompt = async (...args) => {
21
+ const { createPrompt, useKeypress, usePagination, useState, isDownKey, isEnterKey, isUpKey } = await import('@inquirer/core');
22
+ return createPrompt((config, done) => {
23
+ const [active, setActive] = useState(0);
24
+ const choices = config.choices;
24
25
 
25
- useKeypress((key) => {
26
- if (key.name === 'q' || key.name === 'escape') {
27
- done(undefined);
28
- return;
29
- }
30
- if (isEnterKey(key)) {
31
- done(choices[active].value);
32
- return;
33
- }
34
- if (isUpKey(key) && active > 0) setActive(active - 1);
35
- if (isDownKey(key) && active < choices.length - 1) setActive(active + 1);
36
- });
26
+ useKeypress((key) => {
27
+ if (key.name === 'q' || key.name === 'escape') {
28
+ done(undefined);
29
+ return;
30
+ }
31
+ if (isEnterKey(key)) {
32
+ done(choices[active].value);
33
+ return;
34
+ }
35
+ if (isUpKey(key) && active > 0) setActive(active - 1);
36
+ if (isDownKey(key) && active < choices.length - 1) setActive(active + 1);
37
+ });
37
38
 
38
- const page = usePagination({
39
- items: choices,
40
- active,
41
- pageSize: choices.length,
42
- loop: false,
43
- renderItem: ({ item, isActive }) => `${isActive ? '❯' : ' '} ${item.name}`,
44
- });
45
- return `${config.message}\n${page}\n\n↑↓ navigate • ⏎ select • q/Esc exit`;
46
- });
39
+ const page = usePagination({
40
+ items: choices,
41
+ active,
42
+ pageSize: choices.length,
43
+ loop: false,
44
+ renderItem: ({ item, isActive }) => `${isActive ? '❯' : ' '} ${item.name}`,
45
+ });
46
+ return `${config.message}\n${page}\n\n↑↓ navigate • ⏎ select • q/Esc exit`;
47
+ })(...args);
48
+ };
47
49
 
48
50
  /** Interactive hub: what do you want to do? Returns an action or undefined on exit. */
49
51
  export async function authHubMenu({ promptFn = authHubPrompt, input, output } = {}) {
@@ -333,7 +333,7 @@ export function skillCmd(wrap) {
333
333
  ? `Where should the jsn skill be installed? (${outdated.length} outdated)`
334
334
  : 'Where should the jsn skill be installed?';
335
335
 
336
- const { checkbox } = await import('@inquirer/prompts');
336
+ const { checkbox } = await import('../prompts.js');
337
337
  const choices = Object.entries(TARGET_NAMES).map(([key, label]) => {
338
338
  const dir = AGENT_SKILL_DIRS[key];
339
339
  const targetPath = key === 'cursor'
@@ -1,4 +1,4 @@
1
- import { checkbox, select, confirm } from '@inquirer/prompts';
1
+ import { checkbox, select, confirm } from '../prompts.js';
2
2
  import { canUsePicker, interactiveList, getStringField } from '../helpers.js';
3
3
  import { declareCapabilities } from '../capabilities.js';
4
4
  import { errUsage } from '../errors.js';
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { editor, confirm } from '@inquirer/prompts';
2
+ import { editor, confirm } from '../prompts.js';
3
3
  import { formatRecordForDisplay, getStringField, interactiveList, assertSafeExactMatch, canPrompt, buildRerunCommand, verifyWriteBack } from '../helpers.js';
4
4
  import { getActiveProfile } from '../config.js';
5
5
  import { FormatAuto } from '../output.js';
@@ -19,14 +19,28 @@ export function getVersion() {
19
19
  }
20
20
  }
21
21
 
22
- export async function checkLatest() {
22
+ const backgroundChecks = new Set();
23
+
24
+ export function cancelBackgroundChecks() {
25
+ for (const controller of backgroundChecks) controller.abort();
26
+ backgroundChecks.clear();
27
+ }
28
+
29
+ export async function checkLatest({ timeout = 1500, background = false } = {}) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeout);
32
+ timer.unref();
33
+ if (background) backgroundChecks.add(controller);
23
34
  try {
24
- const res = await fetch(NPM_REGISTRY);
35
+ const res = await fetch(NPM_REGISTRY, { signal: controller.signal });
25
36
  if (!res.ok) return null;
26
37
  const data = await res.json();
27
38
  return data.version;
28
39
  } catch {
29
40
  return null;
41
+ } finally {
42
+ clearTimeout(timer);
43
+ backgroundChecks.delete(controller);
30
44
  }
31
45
  }
32
46
 
@@ -1,7 +1,7 @@
1
1
  // Terminal orchestration only. Persistence and lifecycle remain in the SDK.
2
2
  import fs from 'node:fs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
- import { input, select, confirm, editor } from '@inquirer/prompts';
4
+ import { input, select, confirm, editor } from './prompts.js';
5
5
  import { canPrompt, interactiveList, getStringField } from './helpers.js';
6
6
  import { FormatAuto } from './output.js';
7
7
  import { errUsage, errConfirmationRequired } from './errors.js';
@@ -35,7 +35,7 @@ export function interactiveDesigner(argv, app) {
35
35
  export async function cancelled(operation) {
36
36
  try { return await operation(); }
37
37
  catch (error) {
38
- if (error === CANCEL || ['ExitPromptError', 'AbortPromptError'].includes(error.name)) return undefined;
38
+ if (error === CANCEL || ['ExitPromptError', 'AbortPromptError', 'PromptCancelled'].includes(error.name)) return undefined;
39
39
  // Keep direct handler errors throwable; opt into the shared JSON renderer at the CLI boundary.
40
40
  if (error.code === 'usage_error') error.jsonEnvelope = true;
41
41
  throw error;
@@ -1,4 +1,5 @@
1
- import yaml from 'js-yaml';
1
+ // js-yaml 5 ships ESM-only named exports; there is no default export.
2
+ import { load as loadYaml, dump as dumpYaml } from 'js-yaml';
2
3
 
3
4
  const DEFAULT_DELIMITERS = ['---', '---'];
4
5
 
@@ -27,7 +28,8 @@ export function parseFrontmatter(input, options = {}) {
27
28
 
28
29
  const closeIndex = frontmatter.indexOf(`\n${close}`);
29
30
  const raw = closeIndex === -1 ? frontmatter : frontmatter.slice(0, closeIndex);
30
- const parsed = yaml.load(raw);
31
+ // js-yaml 5 throws on an empty document; an empty block means no metadata.
32
+ const parsed = raw.trim() === '' ? {} : loadYaml(raw);
31
33
  const data = parsed === undefined ? {} : parsed;
32
34
 
33
35
  if (closeIndex === -1) return { data, content: '' };
@@ -41,7 +43,7 @@ export function parseFrontmatter(input, options = {}) {
41
43
  /** Serialize docs metadata and body using gray-matter-compatible YAML framing. */
42
44
  export function stringifyFrontmatter(body, data, options = {}) {
43
45
  const [open, close] = delimiters(options);
44
- const yamlText = yaml.dump(data, options).trim();
46
+ const yamlText = dumpYaml(data, options).trim();
45
47
  const prefix = yamlText === '{}' ? '' : `${open}\n${yamlText}\n${close}\n`;
46
48
  const content = String(body);
47
49
  return prefix + (content.endsWith('\n') ? content : `${content}\n`);
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
  }
@@ -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)'
186
- : '(type to search)';
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,8 +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);
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');
37
+
38
+ // The editor prompt hands the keyboard to $EDITOR, so Escape belongs to that
39
+ // editor. It lives here so every command imports prompts from one module;
40
+ // cancelling an editor still uses Inquirer's Ctrl+C abort.
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
+ }