@emeryld/manager 0.6.6 → 0.6.7

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.
@@ -53,9 +53,16 @@ async function promptWithReadline(entries, title) {
53
53
  rl.close();
54
54
  }
55
55
  }
56
- function formatInteractiveLines(state, selectedIndex, title) {
56
+ function formatInteractiveLines(state, selectedIndex, title, searchState) {
57
57
  const heading = pageHeading(title, state.page, state.pageCount);
58
58
  const lines = [heading];
59
+ if (searchState?.active) {
60
+ const queryLabel = searchState.query || '(type to filter)';
61
+ const statusLabel = searchState.hasResults
62
+ ? '(enter to run top match)'
63
+ : '(no matches yet)';
64
+ lines.push(colors.dim(`Search: ${queryLabel} ${statusLabel}`));
65
+ }
59
66
  state.options.forEach((option, index) => {
60
67
  const isSelected = index === selectedIndex;
61
68
  const pointer = isSelected ? `${colors.green('➤')} ` : '';
@@ -69,7 +76,12 @@ function formatInteractiveLines(state, selectedIndex, title) {
69
76
  : isSelected
70
77
  ? colors.green(option.entry.displayName)
71
78
  : option.entry.displayName;
72
- lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}`);
79
+ const runHint = searchState?.active &&
80
+ searchState.hasResults &&
81
+ index === 0
82
+ ? colors.dim(' (enter to run)')
83
+ : '';
84
+ lines.push(`${pointer}${numberLabel}. ${option.entry.emoji} ${label} ${colors.dim(option.entry.metaLabel)}${runHint}`);
73
85
  return;
74
86
  }
75
87
  const icon = option.action === 'back'
@@ -85,8 +97,17 @@ function formatInteractiveLines(state, selectedIndex, title) {
85
97
  const navLabel = option.enabled ? baseLabel : colors.dim(baseLabel);
86
98
  lines.push(`${pointer}${numberLabel}. ${icon} ${navLabel}`);
87
99
  });
100
+ if (searchState?.active && !searchState.hasResults) {
101
+ lines.push(colors.yellow('No scripts match your search.'));
102
+ }
88
103
  lines.push('');
89
104
  lines.push(colors.dim(`Use ↑/↓ (or j/k) to move, 1-${PAGE_SIZE} to run, ${PREVIOUS_KEY} prev page (when shown), ${NEXT_KEY} next page (when shown), ${BACK_KEY} back, Enter to confirm, Esc/Ctrl+C to exit.`));
105
+ if (searchState?.active) {
106
+ lines.push(colors.dim('Search mode: type to filter, Backspace clears query or exits, Enter runs the top match.'));
107
+ }
108
+ else {
109
+ lines.push(colors.dim('Press Enter to search scripts by name.'));
110
+ }
90
111
  return lines;
91
112
  }
92
113
  function renderInteractiveList(lines, previousLineCount) {
@@ -109,9 +130,15 @@ export async function promptForScript(entries, title) {
109
130
  }
110
131
  process.stdout.write('\x1b[?25l');
111
132
  return new Promise((resolve) => {
133
+ const baseEntries = entries;
134
+ let filteredEntries = baseEntries;
112
135
  let selectedIndex = 0;
113
136
  let renderedLines = 0;
114
- let state = buildVisibleOptions(entries, 0);
137
+ let state = buildVisibleOptions(filteredEntries, 0);
138
+ let searchActive = false;
139
+ let searchQuery = '';
140
+ let pageBeforeSearch = 0;
141
+ let selectionBeforeSearch = 0;
115
142
  const cleanup = () => {
116
143
  if (renderedLines > 0) {
117
144
  process.stdout.write(`\x1b[${renderedLines}A`);
@@ -135,10 +162,24 @@ export async function promptForScript(entries, title) {
135
162
  console.log();
136
163
  resolve(undefined);
137
164
  };
138
- const setPage = (page) => {
139
- state = buildVisibleOptions(entries, page);
165
+ const rebuildState = (page) => {
166
+ state = buildVisibleOptions(filteredEntries, page);
140
167
  selectedIndex = Math.min(selectedIndex, state.options.length - 1);
141
168
  selectedIndex = Math.max(0, selectedIndex);
169
+ };
170
+ const render = () => {
171
+ const searchState = searchActive
172
+ ? {
173
+ active: true,
174
+ query: searchQuery.trim(),
175
+ hasResults: filteredEntries.length > 0,
176
+ }
177
+ : undefined;
178
+ const lines = formatInteractiveLines(state, selectedIndex, title, searchState);
179
+ renderedLines = renderInteractiveList(lines, renderedLines);
180
+ };
181
+ const setPage = (page) => {
182
+ rebuildState(page);
142
183
  render();
143
184
  };
144
185
  const handleNav = (option) => {
@@ -167,9 +208,47 @@ export async function promptForScript(entries, title) {
167
208
  }
168
209
  handleNav(option);
169
210
  };
170
- const render = () => {
171
- const lines = formatInteractiveLines(state, selectedIndex, title);
172
- renderedLines = renderInteractiveList(lines, renderedLines);
211
+ const matchesSearchTerm = (entry, term) => {
212
+ const haystack = [
213
+ entry.displayName,
214
+ entry.metaLabel,
215
+ entry.script ?? '',
216
+ entry.description ?? '',
217
+ ]
218
+ .join(' ')
219
+ .toLowerCase();
220
+ return haystack.includes(term);
221
+ };
222
+ const applySearch = () => {
223
+ const normalized = searchQuery.trim().toLowerCase();
224
+ if (normalized.length > 0) {
225
+ filteredEntries = baseEntries.filter((entry) => matchesSearchTerm(entry, normalized));
226
+ }
227
+ else {
228
+ filteredEntries = baseEntries;
229
+ }
230
+ rebuildState(0);
231
+ selectedIndex = 0;
232
+ render();
233
+ };
234
+ const enterSearchMode = () => {
235
+ if (searchActive)
236
+ return;
237
+ pageBeforeSearch = state.page;
238
+ selectionBeforeSearch = selectedIndex;
239
+ searchActive = true;
240
+ searchQuery = '';
241
+ applySearch();
242
+ };
243
+ const exitSearchMode = () => {
244
+ if (!searchActive)
245
+ return;
246
+ searchActive = false;
247
+ searchQuery = '';
248
+ filteredEntries = baseEntries;
249
+ state = buildVisibleOptions(filteredEntries, pageBeforeSearch);
250
+ selectedIndex = Math.min(selectionBeforeSearch, state.options.length - 1);
251
+ render();
173
252
  };
174
253
  const onData = (buffer) => {
175
254
  const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
@@ -177,10 +256,39 @@ export async function promptForScript(entries, title) {
177
256
  const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
178
257
  const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
179
258
  const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
259
+ const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
260
+ const isPrintable = buffer.length === 1 && buffer[0] >= 0x20 && buffer[0] <= 0x7e;
180
261
  if (isCtrlC || isEscape) {
181
262
  cleanup();
182
263
  process.exit(1);
183
264
  }
265
+ if (searchActive) {
266
+ if (isEnter) {
267
+ if (filteredEntries.length > 0) {
268
+ commitSelection(filteredEntries[0]);
269
+ }
270
+ else {
271
+ process.stdout.write('\x07');
272
+ }
273
+ return;
274
+ }
275
+ if (isBackspace) {
276
+ if (searchQuery.length > 0) {
277
+ searchQuery = searchQuery.slice(0, -1);
278
+ applySearch();
279
+ }
280
+ else {
281
+ exitSearchMode();
282
+ }
283
+ return;
284
+ }
285
+ if (isPrintable) {
286
+ searchQuery += String.fromCharCode(buffer[0]);
287
+ applySearch();
288
+ return;
289
+ }
290
+ return;
291
+ }
184
292
  if (isArrowUp ||
185
293
  (buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
186
294
  selectedIndex =
@@ -195,7 +303,7 @@ export async function promptForScript(entries, title) {
195
303
  return;
196
304
  }
197
305
  if (isEnter) {
198
- activateOption(state.options[selectedIndex]);
306
+ enterSearchMode();
199
307
  return;
200
308
  }
201
309
  if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
@@ -211,10 +319,10 @@ export async function promptForScript(entries, title) {
211
319
  ((buffer[0] >= 0x41 && buffer[0] <= 0x5a) ||
212
320
  (buffer[0] >= 0x61 && buffer[0] <= 0x7a))) {
213
321
  const char = String.fromCharCode(buffer[0]).toLowerCase();
214
- const foundIndex = entries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
322
+ const foundIndex = baseEntries.findIndex((entry) => entry.displayName.toLowerCase().startsWith(char));
215
323
  if (foundIndex !== -1) {
216
324
  const page = Math.floor(foundIndex / PAGE_SIZE);
217
- state = buildVisibleOptions(entries, page);
325
+ state = buildVisibleOptions(filteredEntries, page);
218
326
  selectedIndex = foundIndex % PAGE_SIZE;
219
327
  render();
220
328
  }
@@ -14,7 +14,7 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
14
14
  const [firstArg, ...restArgs] = args;
15
15
  if (firstArg === '--list' || firstArg === '-l') {
16
16
  printScriptList(normalized, title);
17
- return;
17
+ return false;
18
18
  }
19
19
  if (firstArg === '--help' || firstArg === '-h') {
20
20
  console.log(colors.magenta('Usage:'));
@@ -22,19 +22,21 @@ export async function runHelperCli({ scripts, title = 'Helper CLI', argv = proce
22
22
  console.log('\nFlags:');
23
23
  console.log(' --list Show available scripts');
24
24
  console.log(' --help Show this information');
25
- return;
25
+ return false;
26
26
  }
27
27
  const argLooksLikeScript = firstArg && !firstArg.startsWith('-');
28
28
  if (argLooksLikeScript) {
29
29
  const requested = findScriptEntry(normalized, firstArg);
30
30
  if (requested) {
31
31
  await runEntry(requested, restArgs);
32
- return;
32
+ return true;
33
33
  }
34
34
  console.log(colors.yellow(`Unknown script "${firstArg}". Falling back to interactive selection…`));
35
35
  }
36
36
  const selection = await promptForScript(normalized, title);
37
37
  if (selection) {
38
38
  await runEntry(selection, []);
39
+ return true;
39
40
  }
41
+ return false;
40
42
  }
package/dist/menu.js CHANGED
@@ -14,8 +14,7 @@ function formatKindLabel(kind) {
14
14
  return 'CLI';
15
15
  return `${kind.charAt(0).toUpperCase()}${kind.slice(1)}`;
16
16
  }
17
- function makeManagerStepEntries(targets, packages, state, options) {
18
- const includeBack = options?.includeBack ?? true;
17
+ function makeManagerStepEntries(targets, packages, state) {
19
18
  return [
20
19
  {
21
20
  name: 'update dependencies',
@@ -103,18 +102,6 @@ function makeManagerStepEntries(targets, packages, state, options) {
103
102
  state.lastStep = 'full';
104
103
  },
105
104
  },
106
- ...(includeBack
107
- ? [
108
- {
109
- name: 'back',
110
- emoji: '↩️',
111
- description: 'Pick packages again',
112
- handler: async () => {
113
- state.lastStep = 'back';
114
- },
115
- },
116
- ]
117
- : []),
118
105
  ];
119
106
  }
120
107
  function makeDockerEntry(pkg) {
@@ -207,19 +194,7 @@ export async function runStepLoop(targets, packages) {
207
194
  emoji: globalEmoji,
208
195
  description: 'update/test/build/publish',
209
196
  handler: async () => {
210
- const managerEntries = [
211
- ...makeManagerStepEntries(targets, packages, state, {
212
- includeBack: false,
213
- }),
214
- {
215
- name: 'back',
216
- emoji: '↩️',
217
- description: 'Return to package menu',
218
- handler: () => {
219
- state.lastStep = undefined;
220
- },
221
- },
222
- ];
197
+ const managerEntries = makeManagerStepEntries(targets, packages, state);
223
198
  await runHelperCli({
224
199
  title: `Manager actions for ${pkg.name}`,
225
200
  scripts: managerEntries,
@@ -236,38 +211,23 @@ export async function runStepLoop(targets, packages) {
236
211
  console.log(colors.yellow(`No package.json scripts found for ${pkg.name}.`));
237
212
  return;
238
213
  }
239
- const scriptsMenu = [
240
- ...scriptEntries,
241
- {
242
- name: 'back',
243
- emoji: '↩️',
244
- description: 'Return to package menu',
245
- handler: () => { },
246
- },
247
- ];
248
214
  await runHelperCli({
249
215
  title: `${pkg.name} scripts`,
250
- scripts: scriptsMenu,
216
+ scripts: scriptEntries,
251
217
  argv: [],
252
218
  });
253
219
  },
254
220
  },
255
- {
256
- name: 'back',
257
- emoji: '↩️',
258
- description: 'Pick packages again',
259
- handler: () => {
260
- state.lastStep = 'back';
261
- },
262
- },
263
221
  ];
264
- await runHelperCli({
222
+ const ranScript = await runHelperCli({
265
223
  title: `Actions for ${pkg.name}`,
266
224
  scripts: entries,
267
225
  argv: [],
268
226
  });
269
- if (state.lastStep === 'back')
227
+ if (!ranScript) {
228
+ state.lastStep = 'back';
270
229
  return state.lastStep;
230
+ }
271
231
  // loop to keep showing menu
272
232
  }
273
233
  }
@@ -279,11 +239,15 @@ export async function runStepLoop(targets, packages) {
279
239
  while (true) {
280
240
  state.lastStep = undefined;
281
241
  const entries = makeManagerStepEntries(targets, packages, state);
282
- await runHelperCli({
242
+ const ranScript = await runHelperCli({
283
243
  title: `Actions for ${targets.length === 1 ? targets[0].name : 'selected packages'}`,
284
244
  scripts: entries,
285
245
  argv: [], // <- key change
286
246
  });
247
+ if (!ranScript) {
248
+ state.lastStep = 'back';
249
+ return state.lastStep;
250
+ }
287
251
  if (state.lastStep === 'back')
288
252
  return state.lastStep;
289
253
  // keep looping to show menu again
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",